pull/429/head
Hunter Long 2020-03-09 22:24:35 -07:00
parent f62dcd4336
commit e0288b21a4
66 changed files with 1382 additions and 390 deletions

View File

@ -11,12 +11,10 @@ An easy to use Status Page for your websites and applications. Statping will aut
[![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/statping/statping) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statping/general) [![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/) [![GoDoc](https://godoc.org/github.com/golang/gddo?status.svg)](https://godoc.org/github.com/statping/statping) [![Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg)](https://gitter.im/statping/general) [![](https://images.microbadger.com/badges/image/hunterlong/statping.svg)](https://microbadger.com/images/hunterlong/statping) [![Docker Pulls](https://img.shields.io/docker/pulls/hunterlong/statping.svg)](https://hub.docker.com/r/hunterlong/statping/builds/)
## A Future-Proof Status Page <img align="left" width="320" height="235" src="https://img.cjx.io/statupsiterun.gif">
Statping strives to remain future-proof and remain intact if a failure is created. Your Statping service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.
<p align="center"> <h2>A Future-Proof Status Page</h2>
<img width="80%" src="https://img.cjx.io/statupsiterun.gif"> Statping strives to remain future-proof and remain intact if a failure is created. Your Statping service should not be running on the same instance you're trying to monitor. If your server crashes your Status Page should still remaining online to notify your users of downtime.
</p>
## Lightweight and Fast ## Lightweight and Fast
Statping is a very lightweight application and is available for Linux, Mac, and Windows. The Docker image is only ~16Mb so you know that this application won't be filling up your hard drive space. Statping is a very lightweight application and is available for Linux, Mac, and Windows. The Docker image is only ~16Mb so you know that this application won't be filling up your hard drive space.

View File

@ -28,7 +28,6 @@ import (
"github.com/statping/statping/source" "github.com/statping/statping/source"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/statping/statping/database"
"github.com/statping/statping/handlers" "github.com/statping/statping/handlers"
"github.com/statping/statping/types/configs" "github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core" "github.com/statping/statping/types/core"
@ -47,6 +46,8 @@ var (
port int port int
log = utils.Log.WithField("type", "cmd") log = utils.Log.WithField("type", "cmd")
httpServer = make(chan bool) httpServer = make(chan bool)
confgs *configs.DbConfig
) )
func init() { func init() {
@ -119,29 +120,29 @@ func main() {
log.Errorln(err) log.Errorln(err)
} }
c, err := configs.LoadConfigs() confgs, err = configs.LoadConfigs()
if err != nil { if err != nil {
if err := SetupMode(); err != nil { if err := SetupMode(); err != nil {
exit(err) exit(err)
} }
} }
if err = configs.ConnectConfigs(c); err != nil { if err = configs.ConnectConfigs(confgs); err != nil {
exit(err) exit(err)
} }
exists := database.DB().HasTable("core") exists := confgs.Db.HasTable("core")
if !exists { if !exists {
if err := c.DropDatabase(); err != nil { if err := confgs.DropDatabase(); err != nil {
exit(errors.Wrap(err, "error dropping database")) exit(errors.Wrap(err, "error dropping database"))
} }
if err := configs.CreateDatabase(); err != nil { if err := confgs.CreateDatabase(); err != nil {
exit(errors.Wrap(err, "error creating database")) exit(errors.Wrap(err, "error creating database"))
} }
if err := configs.CreateAdminUser(c); err != nil { if err := configs.CreateAdminUser(confgs); err != nil {
exit(errors.Wrap(err, "error creating default admin user")) exit(errors.Wrap(err, "error creating default admin user"))
} }
@ -151,7 +152,7 @@ func main() {
} }
if err := c.MigrateDatabase(); err != nil { if err := confgs.MigrateDatabase(); err != nil {
exit(err) exit(err)
} }
@ -170,7 +171,7 @@ func main() {
func Close() { func Close() {
sentry.Flush(3 * time.Second) sentry.Flush(3 * time.Second)
utils.CloseLogs() utils.CloseLogs()
database.Close() confgs.Close()
} }
func SetupMode() error { func SetupMode() error {

View File

@ -18,9 +18,7 @@ const (
TIME_DAY = "2006-01-02" TIME_DAY = "2006-01-02"
) )
var ( var database Database
database Database
)
// Database is an interface which DB implements // Database is an interface which DB implements
type Database interface { type Database interface {
@ -112,39 +110,35 @@ type Database interface {
DbType() string DbType() string
} }
func DB() Database {
return database
}
func (it *Db) DbType() string { func (it *Db) DbType() string {
return it.Database.Dialect().GetName() return it.Database.Dialect().GetName()
} }
func Close() error { func Close(db Database) error {
if database == nil { if db == nil {
return nil return nil
} }
return database.Close() return db.Close()
} }
func LogMode(b bool) Database { func LogMode(db Database, b bool) Database {
return database.LogMode(b) return db.LogMode(b)
} }
func Begin(model interface{}) Database { func Begin(db Database, model interface{}) Database {
if all, ok := model.(string); ok { if all, ok := model.(string); ok {
if all == "migration" { if all == "migration" {
return database.Begin() return db.Begin()
} }
} }
return database.Model(model).Begin() return db.Model(model).Begin()
} }
func Available() bool { func Available(db Database) bool {
if database == nil { if db == nil {
return false return false
} }
if err := database.DB().Ping(); err != nil { if err := db.DB().Ping(); err != nil {
return false return false
} }
return true return true
@ -179,11 +173,15 @@ func Openw(dialect string, args ...interface{}) (db Database, err error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
db = Wrap(gormdb) database = Wrap(gormdb)
database = db
return database, err return database, err
} }
func OpenTester() (Database, error) {
newDb, err := Openw("sqlite3", ":memory:?cache=shared")
return newDb, err
}
// Wrap wraps gorm.DB in an interface // Wrap wraps gorm.DB in an interface
func Wrap(db *gorm.DB) Database { func Wrap(db *gorm.DB) Database {
return &Db{ return &Db{

View File

@ -50,11 +50,11 @@ var (
ByAverage = func(column string, multiplier int) By { ByAverage = func(column string, multiplier int) By {
switch database.DbType() { switch database.DbType() {
case "mysql": case "mysql":
return By(fmt.Sprintf("CAST(AVG(%s)*%d as UNSIGNED) as amount", column, multiplier)) return By(fmt.Sprintf("CAST(AVG(%s) as UNSIGNED) as amount", column))
case "postgres": case "postgres":
return By(fmt.Sprintf("cast(AVG(%s)*%d as int) as amount", column, multiplier)) return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
default: default:
return By(fmt.Sprintf("cast(AVG(%s)*%d as int) as amount", column, multiplier)) return By(fmt.Sprintf("cast(AVG(%s) as int) as amount", column))
} }
} }
) )
@ -157,7 +157,7 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
limit = 10000 limit = 10000
} }
db := o.Db() q := o.Db()
if grouping == "" { if grouping == "" {
grouping = "1h" grouping = "1h"
@ -176,7 +176,7 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
Limit: int(limit), Limit: int(limit),
Offset: int(offset), Offset: int(offset),
FillEmpty: fill, FillEmpty: fill,
db: db, db: q,
} }
if startField == 0 { if startField == 0 {
@ -190,18 +190,18 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
} }
if query.Limit != 0 { if query.Limit != 0 {
db = db.Limit(query.Limit) q = q.Limit(query.Limit)
} }
if query.Offset > 0 { if query.Offset > 0 {
db = db.Offset(query.Offset) q = q.Offset(query.Offset)
} }
db = db.Where("created_at BETWEEN ? AND ?", db.FormatTime(query.Start), db.FormatTime(query.End)) q = q.Where("created_at BETWEEN ? AND ?", q.FormatTime(query.Start), q.FormatTime(query.End))
if query.Order != "" { if query.Order != "" {
db = db.Order(query.Order) q = q.Order(query.Order)
} }
query.db = db query.db = q
return query return query
} }

View File

@ -59,8 +59,8 @@ func databaseMaintence(dur time.Duration) {
// DeleteAllSince will delete a specific table's records based on a time. // DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) { func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, database.FormatTime(date)) sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, database.FormatTime(date))
db := database.Exec(sql) q := database.Exec(sql)
if db.Error() != nil { if q.Error() != nil {
log.Warnln(db.Error()) log.Warnln(q.Error())
} }
} }

View File

@ -5,6 +5,22 @@ HTML,BODY {
background-color: $background-color; background-color: $background-color;
} }
.chartmarker {
background-color: white;
padding: 5px;
}
.chartmarker SPAN {
font-size: 11pt;
display: block;
color: #8b8b8b;
}
.apexcharts-tooltip {
box-shadow: none;
}
.contain-card { .contain-card {
.card-header { .card-header {

View File

View File

@ -4,19 +4,20 @@
<script> <script>
import Api from "../../API" import Api from "../../API"
const timeoptions = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' };
const axisOptions = { const axisOptions = {
labels: { labels: {
show: false show: false
}, },
crosshairs: { crosshairs: {
show: false show: true
}, },
lines: { lines: {
show: false show: false
}, },
tooltip: { tooltip: {
enabled: false enabled: true
}, },
axisTicks: { axisTicks: {
show: false show: false
@ -25,7 +26,7 @@
show: false show: false
}, },
marker: { marker: {
show: false show: true
} }
}; };
@ -47,6 +48,9 @@
showing: false, showing: false,
data: [], data: [],
chartOptions: { chartOptions: {
noData: {
text: 'Loading...'
},
chart: { chart: {
height: 210, height: 210,
width: "100%", width: "100%",
@ -76,21 +80,47 @@
left: -10, left: -10,
} }
}, },
dropShadow: {
enabled: false,
},
xaxis: { xaxis: {
type: "datetime", type: "datetime",
...axisOptions labels: {
show: false
},
}, },
yaxis: { yaxis: {
...axisOptions labels: {
show: false
},
}, },
tooltip: { tooltip: {
enabled: false, theme: false,
marker: { enabled: true,
show: false, markers: {
size: 0
},
custom: function({series, seriesIndex, dataPointIndex, w}) {
let service = w.globals.seriesNames[0];
let ts = w.globals;
window.console.log(ts);
let val = series[seriesIndex][dataPointIndex];
if (val > 1000) {
val = (val * 0.1).toFixed(0) + " milliseconds"
} else {
val = (val * 0.01).toFixed(0) + " microseconds"
}
return `<div class="chartmarker"><span>${service} Average Response</span> <span class="font-3">${val}</span></div>`
},
fixed: {
enabled: true,
position: 'topRight',
offsetX: -30,
offsetY: 0,
}, },
x: { x: {
show: false, show: false,
} },
}, },
legend: { legend: {
show: false, show: false,
@ -132,6 +162,7 @@
}, },
methods: { methods: {
async chartHits(group) { async chartHits(group) {
window.console.log(this.service.created_at)
this.data = await Api.service_hits(this.service.id, this.toUnix(this.service.created_at), this.toUnix(new Date()), group, false) this.data = await Api.service_hits(this.service.id, this.toUnix(this.service.created_at), this.toUnix(new Date()), group, false)
if (this.data.length === 0 && group !== "1h") { if (this.data.length === 0 && group !== "1h") {
@ -139,7 +170,7 @@
} }
this.series = [{ this.series = [{
name: this.service.name, name: this.service.name,
...this.convertToChartData(this.data) ...this.convertToChartData(this.data, 0.01)
}] }]
this.ready = true this.ready = true
} }

View File

@ -127,13 +127,13 @@ func apiClearCacheHandler(w http.ResponseWriter, r *http.Request) {
returnJson(output, w, r) returnJson(output, w, r)
} }
func sendErrorJson(err error, w http.ResponseWriter, r *http.Request) { func sendErrorJson(err error, w http.ResponseWriter, r *http.Request, statusCode ...int) {
log.Warnln(fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error())) log.Warnln(fmt.Errorf("sending error response for %v: %v", r.URL.String(), err.Error()))
output := apiResponse{ output := apiResponse{
Status: "error", Status: "error",
Error: err.Error(), Error: err.Error(),
} }
returnJson(output, w, r) returnJson(output, w, r, statusCode...)
} }
func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *http.Request) { func sendJsonAction(obj interface{}, method string, w http.ResponseWriter, r *http.Request) {

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -43,7 +43,7 @@ func apiAllGroupHandler(r *http.Request) interface{} {
func apiGroupHandler(w http.ResponseWriter, r *http.Request) { func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
group, err := selectGroup(r) group, err := selectGroup(r)
if err != nil { if err != nil {
sendErrorJson(errors.Wrap(err, "group not found"), w, r) sendErrorJson(errors.Wrap(err, "group not found"), w, r, http.StatusNotFound)
return return
} }
returnJson(group, w, r) returnJson(group, w, r)

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -286,8 +286,12 @@ func executeJSResponse(w http.ResponseWriter, r *http.Request, file string, data
//} //}
} }
func returnJson(d interface{}, w http.ResponseWriter, r *http.Request) { func returnJson(d interface{}, w http.ResponseWriter, r *http.Request, statusCode ...int) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
if len(statusCode) != 0 {
code := statusCode[0]
w.WriteHeader(code)
}
json.NewEncoder(w).Encode(d) json.NewEncoder(w).Encode(d)
} }

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (
@ -10,7 +8,7 @@ import (
) )
func TestAttachment(t *testing.T) { func TestAttachment(t *testing.T) {
err := notifiers.AttachNotifiers() err := notifiers.Migrate()
require.Nil(t, err) require.Nil(t, err)
} }

View File

@ -91,14 +91,14 @@ func prometheusHandler(w http.ResponseWriter, r *http.Request) {
PrometheusComment(fmt.Sprintf("Service #%d '%s':", ser.Id, ser.Name)) PrometheusComment(fmt.Sprintf("Service #%d '%s':", ser.Id, ser.Name))
PrometheusExportKey("service_failures", id, name, ser.AllFailures().Count()) PrometheusExportKey("service_failures", id, name, ser.AllFailures().Count())
PrometheusExportKey("service_latency", id, name, ser.Latency*100) PrometheusExportKey("service_latency", id, name, ser.Latency)
PrometheusExportKey("service_online", id, name, online) PrometheusExportKey("service_online", id, name, online)
PrometheusExportKey("service_status_code", id, name, ser.LastStatusCode) PrometheusExportKey("service_status_code", id, name, ser.LastStatusCode)
PrometheusExportKey("service_response_length", id, name, len([]byte(ser.LastResponse))) PrometheusExportKey("service_response_length", id, name, len([]byte(ser.LastResponse)))
PrometheusExportKey("service_ping_time", id, name, ser.PingTime) PrometheusExportKey("service_ping_time", id, name, ser.PingTime)
PrometheusExportKey("service_last_latency", id, name, ser.LastLatency) PrometheusExportKey("service_last_latency", id, name, ser.LastLatency)
PrometheusExportKey("service_last_lookup", id, name, ser.LastLookupTime) PrometheusExportKey("service_last_lookup", id, name, ser.LastLookupTime)
PrometheusExportKey("service_last_check", id, name, time.Now().Sub(ser.LastCheck).Milliseconds()) PrometheusExportKey("service_last_check", id, name, utils.Now().Sub(ser.LastCheck).Milliseconds())
//PrometheusExportKey("service_online_seconds", id, name, ser.SecondsOnline) //PrometheusExportKey("service_online_seconds", id, name, ser.SecondsOnline)
//PrometheusExportKey("service_offline_seconds", id, name, ser.SecondsOffline) //PrometheusExportKey("service_offline_seconds", id, name, ser.SecondsOffline)

View File

@ -89,11 +89,11 @@ func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) { func apiServiceUpdateHandler(w http.ResponseWriter, r *http.Request) {
service, err := serviceByID(r) service, err := serviceByID(r)
if err != nil { if err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r, http.StatusNotFound)
return return
} }
if err := DecodeJSON(r, &service); err != nil { if err := DecodeJSON(r, &service); err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r, http.StatusBadRequest)
return return
} }

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -17,7 +17,6 @@ package handlers
import ( import (
"errors" "errors"
"github.com/statping/statping/database"
"github.com/statping/statping/notifiers" "github.com/statping/statping/notifiers"
"github.com/statping/statping/types/configs" "github.com/statping/statping/types/configs"
"github.com/statping/statping/types/core" "github.com/statping/statping/types/core"
@ -62,14 +61,14 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
exists := database.DB().HasTable("core") exists := confgs.Db.HasTable("core")
if !exists { if !exists {
if err := confgs.DropDatabase(); err != nil { if err := confgs.DropDatabase(); err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }
if err := configs.CreateDatabase(); err != nil { if err := confgs.CreateDatabase(); err != nil {
sendErrorJson(err, w, r) sendErrorJson(err, w, r)
return return
} }

View File

@ -1,5 +1,3 @@
// +build int
package handlers package handlers
import ( import (

View File

@ -20,9 +20,9 @@ package source
import ( import (
"fmt" "fmt"
"github.com/GeertJohan/go.rice" "github.com/GeertJohan/go.rice"
"github.com/statping/statping/utils"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/russross/blackfriday/v2" "github.com/russross/blackfriday/v2"
"github.com/statping/statping/utils"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"

View File

@ -0,0 +1,117 @@
package checkins
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
var testCheckin = &Checkin{
ServiceId: 1,
Name: "Test Checkin",
Interval: 60,
GracePeriod: 10,
ApiKey: "tHiSiSaTeStXXX",
CreatedAt: utils.Now(),
UpdatedAt: utils.Now(),
LastHitTime: utils.Now().Add(-15 * time.Second),
}
var testCheckinHits = []*CheckinHit{{
Checkin: 1,
From: "0.0.0.0.0",
CreatedAt: utils.Now().Add(-30 * time.Second),
}, {
Checkin: 2,
From: "0.0.0.0",
CreatedAt: utils.Now().Add(-180 * time.Second),
}}
var testApiKey string
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&Checkin{}, &CheckinHit{}, &failures.Failure{})
db.Create(&testCheckin)
for _, v := range testCheckinHits {
db.Create(&v)
}
assert.True(t, db.HasTable(&Checkin{}))
assert.True(t, db.HasTable(&CheckinHit{}))
assert.True(t, db.HasTable(&failures.Failure{}))
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "Test Checkin", item.Name)
assert.NotEmpty(t, item.ApiKey)
testApiKey = item.ApiKey
}
func TestFindByAPI(t *testing.T) {
item, err := FindByAPI(testApiKey)
require.Nil(t, err)
assert.Equal(t, "Test Checkin", item.Name)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestCreate(t *testing.T) {
example := &Checkin{
Name: "Example 2",
}
err := example.Create()
example.Close()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "Example 2", example.Name)
assert.NotZero(t, example.CreatedAt)
assert.NotEmpty(t, example.ApiKey)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Name = "Updated"
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "Updated", item.Name)
item.Close()
}
func TestCheckin_Expected(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
expected := item.Expected()
assert.GreaterOrEqual(t, expected.Seconds(), float64(29))
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(2)
require.Nil(t, err)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -5,48 +5,57 @@ import (
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
) )
func DB() database.Database { var db database.Database
return database.DB().Model(&Checkin{}) var dbHits database.Database
}
func DBhits() database.Database { func SetDB(database database.Database) {
return database.DB().Model(&CheckinHit{}) db = database.Model(&Checkin{})
dbHits = database.Model(&CheckinHit{})
} }
func Find(id int64) (*Checkin, error) { func Find(id int64) (*Checkin, error) {
var checkin Checkin var checkin Checkin
db := DB().Where("id = ?", id).Find(&checkin) q := db.Where("id = ?", id).Find(&checkin)
return &checkin, db.Error() return &checkin, q.Error()
} }
func FindByAPI(key string) (*Checkin, error) { func FindByAPI(key string) (*Checkin, error) {
var checkin Checkin var checkin Checkin
db := DB().Where("api = ?", key).Find(&checkin) q := db.Where("api_key = ?", key).Find(&checkin)
return &checkin, db.Error() return &checkin, q.Error()
} }
func All() []*Checkin { func All() []*Checkin {
var checkins []*Checkin var checkins []*Checkin
DB().Find(&checkins) db.Find(&checkins)
return checkins return checkins
} }
func (c *Checkin) Create() error { func (c *Checkin) Create() error {
c.ApiKey = utils.RandomString(7) c.ApiKey = utils.RandomString(7)
db := DB().Create(c) q := db.Create(c)
c.Start() c.Start()
go c.CheckinRoutine() go c.CheckinRoutine()
return db.Error() return q.Error()
} }
func (c *Checkin) Update() error { func (c *Checkin) Update() error {
db := DB().Update(c) q := db.Update(c)
return db.Error() return q.Error()
} }
func (c *Checkin) Delete() error { func (c *Checkin) Delete() error {
c.Close() c.Close()
db := DB().Delete(c) q := dbHits.Where("checkin = ?", c.Id).Delete(&CheckinHit{})
return db.Error() if err := q.Error(); err != nil {
return err
}
q = db.Model(&Checkin{}).Delete(c)
return q.Error()
} }
//func (c *Checkin) AfterDelete() error {
// //q := dbHits.Where("checkin = ?", c.Id).Delete(&CheckinHit{})
// return q.Error()
//}

View File

@ -2,28 +2,28 @@ package checkins
func (c *Checkin) LastHit() *CheckinHit { func (c *Checkin) LastHit() *CheckinHit {
var hit CheckinHit var hit CheckinHit
DBhits().Where("checkin = ?", c.Id).Limit(1).Find(&hit) dbHits.Where("checkin = ?", c.Id).Limit(1).Find(&hit)
return &hit return &hit
} }
func (c *Checkin) Hits() []*CheckinHit { func (c *Checkin) Hits() []*CheckinHit {
var hits []*CheckinHit var hits []*CheckinHit
DBhits().Where("checkin = ?", c.Id).Find(&hits) dbHits.Where("checkin = ?", c.Id).Find(&hits)
c.AllHits = hits c.AllHits = hits
return hits return hits
} }
func (c *CheckinHit) Create() error { func (c *CheckinHit) Create() error {
db := DBhits().Create(c) q := dbHits.Create(c)
return db.Error() return q.Error()
} }
func (c *CheckinHit) Update() error { func (c *CheckinHit) Update() error {
db := DBhits().Update(c) q := dbHits.Update(c)
return db.Error() return q.Error()
} }
func (c *CheckinHit) Delete() error { func (c *CheckinHit) Delete() error {
db := DBhits().Delete(c) q := dbHits.Delete(c)
return db.Error() return q.Error()
} }

View File

@ -7,7 +7,7 @@ import (
func (c *Checkin) CreateFailure(f *failures.Failure) error { func (c *Checkin) CreateFailure(f *failures.Failure) error {
f.Checkin = c.Id f.Checkin = c.Id
return failures.DB().Create(&f).Error() return failures.DB().Create(f).Error()
} }
func (c *Checkin) FailuresColumnID() (string, int64) { func (c *Checkin) FailuresColumnID() (string, int64) {
@ -19,5 +19,5 @@ func (c *Checkin) Failures() failures.Failurer {
} }
func (c *Checkin) FailuresSince(t time.Time) failures.Failurer { func (c *Checkin) FailuresSince(t time.Time) failures.Failurer {
return failures.FailuresSince(t, c) return failures.Since(t, c)
} }

View File

@ -0,0 +1,3 @@
package checkins
//go:generate counterfeiter . Checkin

View File

@ -47,7 +47,7 @@ CheckinLoop:
Method: "checkin", Method: "checkin",
Service: c.ServiceId, Service: c.ServiceId,
Checkin: c.Id, Checkin: c.Id,
PingTime: c.Expected().Seconds(), PingTime: c.Expected().Milliseconds(),
CreatedAt: time.Time{}, CreatedAt: time.Time{},
} }

View File

@ -1,7 +1,6 @@
package checkins package checkins
import ( import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/failures" "github.com/statping/statping/types/failures"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"time" "time"
@ -32,24 +31,7 @@ type CheckinHit struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }
// BeforeCreate for checkinHit will set CreatedAt to UTC
func (c *CheckinHit) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
}
return
}
func (c *Checkin) BeforeCreate() (err error) { func (c *Checkin) BeforeCreate() (err error) {
c.ApiKey = utils.RandomString(7) c.ApiKey = utils.RandomString(7)
if c.CreatedAt.IsZero() { return nil
c.CreatedAt = time.Now().UTC()
c.UpdatedAt = time.Now().UTC()
}
return
}
func (c *Checkin) BeforeDelete(tx database.Database) (err error) {
return tx.Where("id = ?", c.ServiceId).
Update("group_id", 0).Error()
} }

View File

@ -5,7 +5,16 @@ import (
"github.com/jinzhu/gorm" "github.com/jinzhu/gorm"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/statping/statping/database" "github.com/statping/statping/database"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/groups"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/types/incidents"
"github.com/statping/statping/types/messages"
"github.com/statping/statping/types/notifications"
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/types/services"
"github.com/statping/statping/types/users" "github.com/statping/statping/types/users"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"os" "os"
@ -15,9 +24,6 @@ import (
// Connect will attempt to connect to the sqlite, postgres, or mysql database // Connect will attempt to connect to the sqlite, postgres, or mysql database
func Connect(configs *DbConfig, retry bool) error { func Connect(configs *DbConfig, retry bool) error {
postgresSSL := os.Getenv("POSTGRES_SSLMODE") postgresSSL := os.Getenv("POSTGRES_SSLMODE")
if database.Available() {
return nil
}
var conn string var conn string
var err error var err error
@ -25,7 +31,7 @@ func Connect(configs *DbConfig, retry bool) error {
case "sqlite", "sqlite3", "memory": case "sqlite", "sqlite3", "memory":
if configs.DbConn == "memory" { if configs.DbConn == "memory" {
conn = "sqlite3" conn = "sqlite3"
configs.DbConn = ":memory" configs.DbConn = ":memory:"
} else { } else {
conn = findDbFile(configs) conn = findDbFile(configs)
configs.SqlFile = conn configs.SqlFile = conn
@ -76,14 +82,31 @@ func Connect(configs *DbConfig, retry bool) error {
if dbSession.DB().Ping() == nil { if dbSession.DB().Ping() == nil {
if utils.VerboseMode >= 4 { if utils.VerboseMode >= 4 {
database.LogMode(true).Debug().SetLogger(gorm.Logger{log}) dbSession.LogMode(true).Debug().SetLogger(gorm.Logger{log})
} }
log.Infoln(fmt.Sprintf("Database %v connection was successful.", configs.DbConn)) log.Infoln(fmt.Sprintf("Database %v connection was successful.", configs.DbConn))
} }
configs.Db = dbSession
initModels(configs.Db)
return err return err
} }
func initModels(db database.Database) {
core.SetDB(db)
services.SetDB(db)
hits.SetDB(db)
failures.SetDB(db)
checkins.SetDB(db)
notifications.SetDB(db)
incidents.SetDB(db)
users.SetDB(db)
messages.SetDB(db)
groups.SetDB(db)
}
func CreateAdminUser(configs *DbConfig) error { func CreateAdminUser(configs *DbConfig) error {
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!")) log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))

View File

@ -76,7 +76,7 @@ func (d *DbConfig) DropDatabase() error {
var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}} var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}}
log.Infoln("Dropping Database Tables...") log.Infoln("Dropping Database Tables...")
for _, t := range DbModels { for _, t := range DbModels {
if err := database.DB().DropTableIfExists(t); err != nil { if err := d.Db.DropTableIfExists(t); err != nil {
return err.Error() return err.Error()
} }
log.Infof("Dropped table: %T\n", t) log.Infof("Dropped table: %T\n", t)
@ -84,19 +84,23 @@ func (d *DbConfig) DropDatabase() error {
return nil return nil
} }
func (d *DbConfig) Close() {
d.Db.Close()
}
// CreateDatabase will CREATE TABLES for each of the Statping elements // CreateDatabase will CREATE TABLES for each of the Statping elements
func CreateDatabase() error { func (d *DbConfig) CreateDatabase() error {
var err error var err error
var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}} var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}}
log.Infoln("Creating Database Tables...") log.Infoln("Creating Database Tables...")
for _, table := range DbModels { for _, table := range DbModels {
if err := database.DB().CreateTable(table); err.Error() != nil { if err := d.Db.CreateTable(table); err.Error() != nil {
return err.Error() return err.Error()
} }
} }
if err := database.DB().Table("core").CreateTable(&core.Core{}); err.Error() != nil { if err := d.Db.Table("core").CreateTable(&core.Core{}); err.Error() != nil {
return err.Error() return err.Error()
} }
log.Infoln("Statping Database Created") log.Infoln("Statping Database Created")

View File

@ -2,7 +2,6 @@ package configs
import ( import (
"fmt" "fmt"
"github.com/statping/statping/database"
"github.com/statping/statping/types/checkins" "github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/core" "github.com/statping/statping/types/core"
"github.com/statping/statping/types/failures" "github.com/statping/statping/types/failures"
@ -51,7 +50,7 @@ func (c *DbConfig) MigrateDatabase() error {
var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}} var DbModels = []interface{}{&services.Service{}, &users.User{}, &hits.Hit{}, &failures.Failure{}, &messages.Message{}, &groups.Group{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &notifications.Notification{}, &incidents.Incident{}, &incidents.IncidentUpdate{}}
log.Infoln("Migrating Database Tables...") log.Infoln("Migrating Database Tables...")
tx := database.Begin("migration") tx := c.Db.Begin()
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
tx.Rollback() tx.Rollback()
@ -75,19 +74,19 @@ func (c *DbConfig) MigrateDatabase() error {
} }
log.Infoln("Statping Database Tables Migrated") log.Infoln("Statping Database Tables Migrated")
if err := database.DB().Model(&hits.Hit{}).AddIndex("idx_service_hit", "service").Error(); err != nil { if err := c.Db.Model(&hits.Hit{}).AddIndex("idx_service_hit", "service").Error(); err != nil {
log.Errorln(err) log.Errorln(err)
} }
if err := database.DB().Model(&hits.Hit{}).AddIndex("hit_created_at", "created_at").Error(); err != nil { if err := c.Db.Model(&hits.Hit{}).AddIndex("hit_created_at", "created_at").Error(); err != nil {
log.Errorln(err) log.Errorln(err)
} }
if err := database.DB().Model(&failures.Failure{}).AddIndex("idx_service_fail", "service").Error(); err != nil { if err := c.Db.Model(&failures.Failure{}).AddIndex("idx_service_fail", "service").Error(); err != nil {
log.Errorln(err) log.Errorln(err)
} }
if err := database.DB().Model(&failures.Failure{}).AddIndex("idx_checkin_fail", "checkin").Error(); err != nil { if err := c.Db.Model(&failures.Failure{}).AddIndex("idx_checkin_fail", "checkin").Error(); err != nil {
log.Errorln(err) log.Errorln(err)
} }
log.Infoln("Database Indexes Created") log.Infoln("Database Indexes Created")

View File

@ -1,13 +1,5 @@
package configs package configs
import (
"github.com/romanyx/polluter"
"github.com/statping/statping/database"
"github.com/statping/statping/utils"
"os"
"testing"
)
//func preparePostgresDB(t *testing.T) (database.Database, error) { //func preparePostgresDB(t *testing.T) (database.Database, error) {
// dbName := fmt.Sprintf("db_%d", time.Now().UnixNano()) // dbName := fmt.Sprintf("db_%d", time.Now().UnixNano())
// db, err := database.Openw("sqlite3", dbName) // db, err := database.Openw("sqlite3", dbName)
@ -17,20 +9,3 @@ import (
// //
// return db, db.Error() // return db, db.Error()
//} //}
func TestSeedDatabase(t *testing.T) {
t.SkipNow()
dir := utils.Directory
f, err := os.Open(dir + "/testdata.yml")
if err != nil {
t.Fatal(err)
}
defer f.Close()
p := polluter.New(polluter.PostgresEngine(database.DB().DB()))
if err := p.Pollute(f); err != nil {
t.Fatalf("failed to pollute: %s", err)
}
}

View File

@ -1,5 +1,7 @@
package configs package configs
import "github.com/statping/statping/database"
const SqliteFilename = "statping.db" const SqliteFilename = "statping.db"
// DbConfig struct is used for the Db connection and creates the 'config.yml' file // DbConfig struct is used for the Db connection and creates the 'config.yml' file
@ -23,4 +25,6 @@ type DbConfig struct {
SqlFile string `yaml:"sqlfile,omitempty" json:"-"` SqlFile string `yaml:"sqlfile,omitempty" json:"-"`
LocalIP string `yaml:"-" json:"-"` LocalIP string `yaml:"-" json:"-"`
filename string `yaml:"-" json:"-"` filename string `yaml:"-" json:"-"`
Db database.Database `yaml:"-" json:"-"`
} }

View File

@ -9,27 +9,29 @@ import (
"time" "time"
) )
func DB() database.Database { var db database.Database
return database.DB().Table("core")
func SetDB(database database.Database) {
db = database.Model(&Core{})
} }
func Select() (*Core, error) { func Select() (*Core, error) {
var c Core var c Core
// SelectCore will return the CoreApp global variable and the settings/configs for Statping // SelectCore will return the CoreApp global variable and the settings/configs for Statping
if !database.Available() { if err := db.DB().Ping(); err != nil {
return nil, errors.New("database has not been initiated yet.") return nil, errors.New("database has not been initiated yet.")
} }
exists := DB().HasTable("core") exists := db.HasTable("core")
if !exists { if !exists {
return nil, errors.New("core database has not been setup yet.") return nil, errors.New("core database has not been setup yet.")
} }
db := DB().Find(&c) q := db.Find(&c)
if db.Error() != nil { if q.Error() != nil {
return nil, db.Error() return nil, db.Error()
} }
App = &c App = &c
App.UseCdn = null.NewNullBool(os.Getenv("USE_CDN") == "true") App.UseCdn = null.NewNullBool(os.Getenv("USE_CDN") == "true")
return App, db.Error() return App, q.Error()
} }
func (c *Core) Create() error { func (c *Core) Create() error {
@ -44,13 +46,13 @@ func (c *Core) Create() error {
Domain: c.Domain, Domain: c.Domain,
MigrationId: time.Now().Unix(), MigrationId: time.Now().Unix(),
} }
db := DB().Create(&newCore) q := db.Create(&newCore)
return db.Error() return q.Error()
} }
func (c *Core) Update() error { func (c *Core) Update() error {
db := DB().Update(c) q := db.Update(c)
return db.Error() return q.Error()
} }
func (c *Core) Delete() error { func (c *Core) Delete() error {
@ -73,6 +75,6 @@ func Sample() error {
Footer: null.NewNullString(""), Footer: null.NewNullString(""),
} }
db := DB().Create(core) q := db.Create(core)
return db.Error() return q.Error()
} }

View File

@ -2,33 +2,39 @@ package failures
import "github.com/statping/statping/database" import "github.com/statping/statping/database"
var db database.Database
func SetDB(database database.Database) {
db = database.Model(&Failure{})
}
func DB() database.Database { func DB() database.Database {
return database.DB().Model(&Failure{}) return db
} }
func Find(id int64) (*Failure, error) { func Find(id int64) (*Failure, error) {
var failure Failure var failure Failure
db := DB().Where("id = ?", id).Find(&failure) q := db.Where("id = ?", id).Find(&failure)
return &failure, db.Error() return &failure, q.Error()
} }
func All() []*Failure { func All() []*Failure {
var failures []*Failure var failures []*Failure
DB().Find(&failures) db.Find(&failures)
return failures return failures
} }
func (f *Failure) Create() error { func (f *Failure) Create() error {
db := DB().Create(f) q := db.Create(f)
return db.Error() return q.Error()
} }
func (f *Failure) Update() error { func (f *Failure) Update() error {
db := DB().Update(f) q := db.Update(f)
return db.Error() return q.Error()
} }
func (f *Failure) Delete() error { func (f *Failure) Delete() error {
db := DB().Delete(f) q := db.Delete(f)
return db.Error() return q.Error()
} }

View File

@ -18,37 +18,54 @@ func (f Failurer) Db() database.Database {
return f.db return f.db
} }
func (f Failurer) First() *Failure {
var fail Failure
f.db.Order("id ASC").Limit(1).Find(&fail)
return &fail
}
func (f Failurer) Last() *Failure {
var fail Failure
f.db.Order("id DESC").Limit(1).Find(&fail)
return &fail
}
func (f Failurer) List() []*Failure { func (f Failurer) List() []*Failure {
var fails []*Failure var fails []*Failure
f.db.Find(&fails) f.db.Find(&fails)
return fails return fails
} }
func (f Failurer) LastAmount(amount int) []*Failure {
var fail []*Failure
f.db.Order("id asc").Limit(amount).Find(&fail)
return fail
}
func (f Failurer) Since(t time.Time) []*Failure {
var fails []*Failure
f.db.Since(t).Find(&fails)
return fails
}
func (f Failurer) Count() int { func (f Failurer) Count() int {
var amount int var amount int
f.db.Count(&amount) f.db.Count(&amount)
return amount return amount
} }
func (f Failurer) Last(amount int) []*Failure { func (f Failurer) DeleteAll() error {
var fails []*Failure q := f.db.Delete(&Failure{})
f.db.Limit(amount).Find(&fails) return q.Error()
return fails
}
func (f Failurer) Since(t time.Time) []*Failure {
var fails []*Failure
f.db.Since(t).Find(&fails)
return fails
} }
func AllFailures(obj ColumnIDInterfacer) Failurer { func AllFailures(obj ColumnIDInterfacer) Failurer {
column, id := obj.FailuresColumnID() column, id := obj.FailuresColumnID()
return Failurer{DB().Where(fmt.Sprintf("%s = ?", column), id)} return Failurer{db.Where(fmt.Sprintf("%s = ?", column), id)}
} }
func FailuresSince(t time.Time, obj ColumnIDInterfacer) Failurer { func Since(t time.Time, obj ColumnIDInterfacer) Failurer {
column, id := obj.FailuresColumnID() column, id := obj.FailuresColumnID()
timestamp := DB().FormatTime(t) timestamp := db.FormatTime(t)
return Failurer{DB().Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)} return Failurer{db.Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)}
} }

View File

@ -14,7 +14,7 @@ import (
) )
func Samples() error { func Samples() error {
tx := DB().Begin() tx := db.Begin()
sg := new(sync.WaitGroup) sg := new(sync.WaitGroup)
createdAt := utils.Now().Add(-3 * types.Day) createdAt := utils.Now().Add(-3 * types.Day)

View File

@ -17,18 +17,10 @@ type Failure struct {
ErrorCode int `gorm:"column:error_code" json:"error_code"` ErrorCode int `gorm:"column:error_code" json:"error_code"`
Service int64 `gorm:"index;column:service" json:"-"` Service int64 `gorm:"index;column:service" json:"-"`
Checkin int64 `gorm:"index;column:checkin" json:"-"` Checkin int64 `gorm:"index;column:checkin" json:"-"`
PingTime float64 `gorm:"column:ping_time" json:"ping"` PingTime int64 `gorm:"column:ping_time" json:"ping"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }
// BeforeCreate for Failure will set CreatedAt to UTC
func (f *Failure) BeforeCreate() (err error) {
if f.CreatedAt.IsZero() {
f.CreatedAt = time.Now().UTC()
}
return
}
type FailSort []Failure type FailSort []Failure
func (s FailSort) Len() int { func (s FailSort) Len() int {

View File

@ -5,35 +5,37 @@ import (
"sort" "sort"
) )
func DB() database.Database { var db database.Database
return database.DB().Model(&Group{})
func SetDB(database database.Database) {
db = database.Model(&Group{})
} }
func Find(id int64) (*Group, error) { func Find(id int64) (*Group, error) {
var group Group var group Group
db := DB().Where("id = ?", id).Find(&group) q := db.Where("id = ?", id).Find(&group)
return &group, db.Error() return &group, q.Error()
} }
func All() []*Group { func All() []*Group {
var groups []*Group var groups []*Group
DB().Find(&groups) db.Find(&groups)
return groups return groups
} }
func (g *Group) Create() error { func (g *Group) Create() error {
db := DB().Create(g) q := db.Create(g)
return db.Error() return q.Error()
} }
func (g *Group) Update() error { func (g *Group) Update() error {
db := DB().Update(g) q := db.Update(g)
return db.Error() return q.Error()
} }
func (g *Group) Delete() error { func (g *Group) Delete() error {
db := DB().Delete(g) q := db.Delete(g)
return db.Error() return q.Error()
} }
// SelectGroups returns all groups // SelectGroups returns all groups

View File

@ -0,0 +1,89 @@
package groups
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/null"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
var example = &Group{
Name: "Example Group",
Public: null.NewNullBool(true),
Order: 1,
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.CreateTable(&Group{})
db.Create(&example)
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "Example Group", item.Name)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestCreate(t *testing.T) {
example := &Group{
Name: "Example 2",
Public: null.NewNullBool(false),
Order: 3,
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "Example 2", example.Name)
assert.NotZero(t, example.CreatedAt)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Name = "Updated"
item.Order = 1
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "Updated", item.Name)
}
func TestSelectGroups(t *testing.T) {
groups := SelectGroups(true, true)
assert.Len(t, groups, 2)
groups = SelectGroups(false, false)
assert.Len(t, groups, 1)
groups = SelectGroups(false, true)
assert.Len(t, groups, 2)
assert.Equal(t, "Updated", groups[0].Name)
assert.Equal(t, "Example 2", groups[1].Name)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(1)
require.Nil(t, err)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -1,13 +1,12 @@
package groups package groups
import ( import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/services" "github.com/statping/statping/types/services"
) )
func (g *Group) Services() []*services.Service { func (g *Group) Services() []*services.Service {
var services []*services.Service var services []*services.Service
database.DB().Where("group = ?", g.Id).Find(&services) db.Where("group = ?", g.Id).Find(&services)
return services return services
} }

View File

@ -7,33 +7,35 @@ import (
var log = utils.Log var log = utils.Log
func DB() database.Database { var db database.Database
return database.DB().Model(&Hit{})
func SetDB(database database.Database) {
db = database.Model(&Hit{})
} }
func Find(id int64) (*Hit, error) { func Find(id int64) (*Hit, error) {
var group Hit var group Hit
db := DB().Where("id = ?", id).Find(&group) q := db.Where("id = ?", id).Find(&group)
return &group, db.Error() return &group, q.Error()
} }
func All() []*Hit { func All() []*Hit {
var hits []*Hit var hits []*Hit
DB().Find(&hits) db.Find(&hits)
return hits return hits
} }
func (h *Hit) Create() error { func (h *Hit) Create() error {
db := DB().Create(h) q := db.Create(h)
return db.Error() return q.Error()
} }
func (h *Hit) Update() error { func (h *Hit) Update() error {
db := DB().Update(h) q := db.Update(h)
return db.Error() return q.Error()
} }
func (h *Hit) Delete() error { func (h *Hit) Delete() error {
db := DB().Delete(h) q := db.Delete(h)
return db.Error() return q.Error()
} }

View File

@ -42,9 +42,9 @@ func (h Hitters) List() []*Hit {
return hits return hits
} }
func (h Hitters) LastCount(amounts int) []*Hit { func (h Hitters) LastAmount(amount int) []*Hit {
var hits []*Hit var hits []*Hit
h.db.Order("id asc").Limit(amounts).Find(&hits) h.db.Order("id asc").Limit(amount).Find(&hits)
return hits return hits
} }
@ -54,18 +54,23 @@ func (h Hitters) Count() int {
return count return count
} }
func (h Hitters) DeleteAll() error {
q := h.db.Delete(&Hit{})
return q.Error()
}
func (h Hitters) Sum() float64 { func (h Hitters) Sum() float64 {
result := struct { result := struct {
amount float64 amount float64
}{0} }{0}
h.db.Select("AVG(latency) as amount").Scan(&result).Debug() h.db.Select("AVG(latency) as amount").Scan(&result)
return result.amount return result.amount
} }
func (h Hitters) Avg() int64 { func (h Hitters) Avg() float64 {
result := struct { result := struct {
amount int64 amount float64
}{0} }{0}
h.db.Select("AVG(latency) as amount").Scan(&result) h.db.Select("AVG(latency) as amount").Scan(&result)
@ -74,11 +79,11 @@ func (h Hitters) Avg() int64 {
func AllHits(obj ColumnIDInterfacer) Hitters { func AllHits(obj ColumnIDInterfacer) Hitters {
column, id := obj.HitsColumnID() column, id := obj.HitsColumnID()
return Hitters{DB().Where(fmt.Sprintf("%s = ?", column), id)} return Hitters{db.Where(fmt.Sprintf("%s = ?", column), id)}
} }
func HitsSince(t time.Time, obj ColumnIDInterfacer) Hitters { func Since(t time.Time, obj ColumnIDInterfacer) Hitters {
column, id := obj.HitsColumnID() column, id := obj.HitsColumnID()
timestamp := DB().FormatTime(t) timestamp := db.FormatTime(t)
return Hitters{DB().Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)} return Hitters{db.Where(fmt.Sprintf("%s = ? AND created_at > ?", column), id, timestamp)}
} }

View File

@ -16,7 +16,7 @@ import (
var SampleHits = 99900. var SampleHits = 99900.
func Samples() error { func Samples() error {
tx := DB().Begin() tx := db.Begin()
sg := new(sync.WaitGroup) sg := new(sync.WaitGroup)
for i := int64(1); i <= 5; i++ { for i := int64(1); i <= 5; i++ {
@ -24,7 +24,7 @@ func Samples() error {
if err != nil { if err != nil {
log.Error(err) log.Error(err)
} }
tx = DB().Begin() tx = db.Begin()
} }
return tx.Error() return tx.Error()
@ -44,8 +44,8 @@ func createHitsAt(db database.Database, serviceID int64, sg *sync.WaitGroup) err
hit := &Hit{ hit := &Hit{
Service: serviceID, Service: serviceID,
Latency: latency, Latency: int64(latency * 10000000),
PingTime: latency * 0.15, PingTime: int64(latency * 5000000),
CreatedAt: createdAt, CreatedAt: createdAt,
} }

View File

@ -6,8 +6,8 @@ import "time"
type Hit struct { type Hit struct {
Id int64 `gorm:"primary_key;column:id" json:"id"` Id int64 `gorm:"primary_key;column:id" json:"id"`
Service int64 `gorm:"column:service" json:"-"` Service int64 `gorm:"column:service" json:"-"`
Latency float64 `gorm:"column:latency" json:"latency"` Latency int64 `gorm:"column:latency" json:"latency"`
PingTime float64 `gorm:"column:ping_time" json:"ping_time"` PingTime int64 `gorm:"column:ping_time" json:"ping_time"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
} }

View File

@ -2,29 +2,35 @@ package incidents
import "github.com/statping/statping/database" import "github.com/statping/statping/database"
var db database.Database
func SetDB(database database.Database) {
db = database.Model(&Incident{})
}
func Find(id int64) (*Incident, error) { func Find(id int64) (*Incident, error) {
var incident Incident var incident Incident
db := database.DB().Model(&Incident{}).Where("id = ?", id).Find(&incident) q := db.Where("id = ?", id).Find(&incident)
return &incident, db.Error() return &incident, q.Error()
} }
func All() []*Incident { func All() []*Incident {
var incidents []*Incident var incidents []*Incident
database.DB().Model(&Incident{}).Find(&incidents) db.Find(&incidents)
return incidents return incidents
} }
func (i *Incident) Create() error { func (i *Incident) Create() error {
db := database.DB().Create(i) q := db.Create(i)
return db.Error() return q.Error()
} }
func (i *Incident) Update() error { func (i *Incident) Update() error {
db := database.DB().Update(i) q := db.Update(i)
return db.Error() return q.Error()
} }
func (i *Incident) Delete() error { func (i *Incident) Delete() error {
db := database.DB().Delete(i) q := db.Delete(i)
return db.Error() return q.Error()
} }

View File

@ -1,25 +1,23 @@
package incidents package incidents
import "github.com/statping/statping/database"
func (i *Incident) Updates() []*IncidentUpdate { func (i *Incident) Updates() []*IncidentUpdate {
var updates []*IncidentUpdate var updates []*IncidentUpdate
database.DB().Model(&IncidentUpdate{}).Where("incident = ?", i.Id).Find(&updates) db.Model(&IncidentUpdate{}).Where("incident = ?", i.Id).Find(&updates)
i.AllUpdates = updates i.AllUpdates = updates
return updates return updates
} }
func (i *IncidentUpdate) Create() error { func (i *IncidentUpdate) Create() error {
db := database.DB().Create(i) q := db.Create(i)
return db.Error() return q.Error()
} }
func (i *IncidentUpdate) Update() error { func (i *IncidentUpdate) Update() error {
db := database.DB().Update(i) q := db.Update(i)
return db.Error() return q.Error()
} }
func (i *IncidentUpdate) Delete() error { func (i *IncidentUpdate) Delete() error {
db := database.DB().Delete(i) q := db.Delete(i)
return db.Error() return q.Error()
} }

View File

@ -0,0 +1,83 @@
package incidents
import (
"github.com/statping/statping/database"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
var example = &Incident{
Title: "Example",
Description: "No description",
ServiceId: 1,
}
var update1 = &IncidentUpdate{
IncidentId: 1,
Message: "First one here",
Type: "update",
}
var update2 = &IncidentUpdate{
IncidentId: 1,
Message: "Second one here",
Type: "update",
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&Incident{}, &IncidentUpdate{})
db.Create(&example)
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "Example", item.Title)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestCreate(t *testing.T) {
example := &Incident{
Title: "Example 2",
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "Example 2", example.Title)
assert.NotZero(t, example.CreatedAt)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Title = "Updated"
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "Updated", item.Title)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(1)
require.Nil(t, err)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -2,33 +2,35 @@ package messages
import "github.com/statping/statping/database" import "github.com/statping/statping/database"
func DB() database.Database { var db database.Database
return database.DB().Model(&Message{})
func SetDB(database database.Database) {
db = database.Model(&Message{})
} }
func Find(id int64) (*Message, error) { func Find(id int64) (*Message, error) {
var message Message var message Message
db := DB().Where("id = ?", id).Find(&message) q := db.Where("id = ?", id).Find(&message)
return &message, db.Error() return &message, q.Error()
} }
func All() []*Message { func All() []*Message {
var messages []*Message var messages []*Message
DB().Find(&messages) db.Find(&messages)
return messages return messages
} }
func (m *Message) Create() error { func (m *Message) Create() error {
db := DB().Create(m) q := db.Create(m)
return db.Error() return q.Error()
} }
func (m *Message) Update() error { func (m *Message) Update() error {
db := DB().Update(m) q := db.Update(m)
return db.Error() return q.Error()
} }
func (m *Message) Delete() error { func (m *Message) Delete() error {
db := DB().Delete(m) q := db.Delete(m)
return db.Error() return q.Error()
} }

View File

@ -0,0 +1,76 @@
package messages
import (
"github.com/statping/statping/database"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
var example = &Message{
Title: "Example Message",
Description: "Description here",
StartOn: utils.Now().Add(10 * time.Minute),
EndOn: utils.Now().Add(15 * time.Minute),
ServiceId: 1,
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.CreateTable(&Message{})
db.Create(&example)
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "Example Message", item.Title)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestCreate(t *testing.T) {
example := &Message{
Title: "Example 2",
Description: "New Message here",
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "Example 2", example.Title)
assert.NotZero(t, example.CreatedAt)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Title = "Updated"
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "Updated", item.Title)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(1)
require.Nil(t, err)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -5,19 +5,21 @@ import (
"github.com/statping/statping/database" "github.com/statping/statping/database"
) )
func DB() database.Database { var db database.Database
return database.DB().Model(&Notification{})
func SetDB(database database.Database) {
db = database.Model(&Notification{})
} }
func Append(n Notifier) { func Append(n Notifier) {
allNotifiers = append(allNotifiers, n) allNotifiers = append(allNotifiers, n)
} }
func Find(name string) (Notifier, error) { func Find(name string) (*Notification, error) {
for _, n := range allNotifiers { for _, n := range allNotifiers {
notif := n.Select() notif := n.Select()
if notif.Name() == name || notif.Method == name { if notif.Name() == name || notif.Method == name {
return n, nil return notif, nil
} }
} }
return nil, errors.New("notifier not found") return nil, errors.New("notifier not found")
@ -29,9 +31,11 @@ func All() []Notifier {
func (n *Notification) Create() error { func (n *Notification) Create() error {
var notif Notification var notif Notification
if DB().Where("method = ?", n.Method).Find(&notif).RecordNotFound() { if db.Where("method = ?", n.Method).Find(&notif).RecordNotFound() {
return DB().Create(n).Error() Append(n)
return db.Create(n).Error()
} }
Append(n)
return nil return nil
} }
@ -44,7 +48,7 @@ func (n *Notification) Update() error {
} else { } else {
n.Close() n.Close()
} }
err := DB().Update(n) err := db.Update(n)
return err.Error() return err.Error()
} }

View File

@ -0,0 +1,135 @@
package notifications
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/null"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
var form1 = NotificationForm{
Type: "text",
Title: "Example Input",
DbField: "Host",
Required: true,
IsHidden: false,
IsList: false,
IsSwitch: false,
}
var form2 = NotificationForm{
Type: "text",
Title: "Example Input 2",
DbField: "ApiKey",
Required: true,
IsHidden: false,
IsList: false,
IsSwitch: false,
}
var example = &exampleNotif{&Notification{
Method: "example",
Enabled: null.NewNullBool(true),
Limits: 3,
Removable: false,
Form: []NotificationForm{form1, form2},
Delay: 30,
}}
type exampleNotif struct {
*Notification
}
func (e *exampleNotif) OnSave() error {
return nil
}
func (e *exampleNotif) Select() *Notification {
return e.Notification
}
func (e *exampleNotif) Send(data interface{}) error {
return nil
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.CreateTable(&Notification{})
db.Create(example.Select())
SetDB(db)
}
func TestFind(t *testing.T) {
Append(example)
itemer, err := Find(example.Method)
require.Nil(t, err)
item := itemer.Select()
require.NotNil(t, item)
assert.Equal(t, "example", item.Method)
assert.Len(t, allNotifiers, 1)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
assert.Len(t, allNotifiers, 1)
}
func TestCreate(t *testing.T) {
assert.Len(t, allNotifiers, 1)
example := &Notification{
Method: "anotherexample",
Title: "Example 2",
Description: "New Message here",
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "anotherexample", example.Method)
assert.Equal(t, "Example 2", example.Title)
assert.NotZero(t, example.CreatedAt)
items := All()
assert.Len(t, items, 2)
assert.Len(t, allNotifiers, 2)
}
func TestUpdate(t *testing.T) {
itemer, err := Find("anotherexample")
require.Nil(t, err)
require.NotNil(t, itemer)
item := itemer.Select()
require.NotNil(t, item)
item.Host = "Updated Host Var"
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "Updated Host Var", item.Host)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
itemer, err := Find("example2")
require.Nil(t, err)
item := itemer.Select()
require.NotNil(t, item)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 2)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -152,7 +152,7 @@ func reverseLogs(input []*NotificationLog) []*NotificationLog {
// SelectNotification returns the Notification struct from the database // SelectNotification returns the Notification struct from the database
func SelectNotification(n Notifier) (*Notification, error) { func SelectNotification(n Notifier) (*Notification, error) {
notifier := n.Select() notifier := n.Select()
err := DB().Where("method = ?", notifier.Method).Find(&notifier) err := db.Where("method = ?", notifier.Method).Find(&notifier)
return notifier, err.Error() return notifier, err.Error()
} }

29
types/null/null_test.go Normal file
View File

@ -0,0 +1,29 @@
package null
import (
"github.com/stretchr/testify/assert"
"testing"
)
func TestNewNullBool(t *testing.T) {
val := NewNullBool(true)
assert.True(t, val.Bool)
val = NewNullBool(false)
assert.False(t, val.Bool)
}
func TestNewNullInt64(t *testing.T) {
val := NewNullInt64(29)
assert.Equal(t, int64(29), val.Int64)
}
func TestNewNullString(t *testing.T) {
val := NewNullString("statping.com")
assert.Equal(t, "statping.com", val.String)
}
func TestNewNullFloat64(t *testing.T) {
val := NewNullFloat64(42.222)
assert.Equal(t, float64(42.222), val.Float64)
}

View File

@ -1,7 +1,6 @@
package services package services
import ( import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/checkins" "github.com/statping/statping/types/checkins"
) )
@ -15,6 +14,6 @@ func CheckinProcess(s *Service) {
func (s *Service) Checkins() []*checkins.Checkin { func (s *Service) Checkins() []*checkins.Checkin {
var chks []*checkins.Checkin var chks []*checkins.Checkin
database.DB().Where("service = ?", s.Id).Find(&chks) db.Where("service = ?", s.Id).Find(&chks)
return chks return chks
} }

View File

@ -10,8 +10,10 @@ import (
var log = utils.Log var log = utils.Log
func DB() database.Database { var db database.Database
return database.DB().Model(&Service{})
func SetDB(database database.Database) {
db = database.Model(&Service{})
} }
func Find(id int64) (*Service, error) { func Find(id int64) (*Service, error) {
@ -24,7 +26,7 @@ func Find(id int64) (*Service, error) {
func all() []*Service { func all() []*Service {
var services []*Service var services []*Service
DB().Find(&services) db.Find(&services)
return services return services
} }
@ -42,17 +44,21 @@ func AllInOrder() []Service {
} }
func (s *Service) Create() error { func (s *Service) Create() error {
err := DB().Create(s) err := db.Create(s)
if err.Error() != nil { if err.Error() != nil {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err)) log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err))
return err.Error() return err.Error()
} }
return nil
}
func (s *Service) AfterCreate() error {
allServices[s.Id] = s allServices[s.Id] = s
return nil return nil
} }
func (s *Service) Update() error { func (s *Service) Update() error {
db := DB().Update(s) q := db.Update(s)
allServices[s.Id] = s allServices[s.Id] = s
@ -68,20 +74,49 @@ func (s *Service) Update() error {
//notifier.OnUpdatedService(s.Service) //notifier.OnUpdatedService(s.Service)
return db.Error() return q.Error()
} }
func (s *Service) Delete() error { func (s *Service) Delete() error {
db := database.DB().Delete(s)
s.Close() s.Close()
delete(allServices, s.Id)
//notifier.OnDeletedService(s.Service)
return db.Error() if err := s.DeleteFailures(); err != nil {
return err
}
if err := s.DeleteHits(); err != nil {
return err
}
delete(allServices, s.Id)
q := db.Model(&Service{}).Delete(s)
//notifier.OnDeletedService(s.Service)
return q.Error()
} }
func (s *Service) DeleteFailures() error { func (s *Service) DeleteFailures() error {
query := database.DB().Exec(`DELETE FROM failures WHERE service = ?`, s.Id) return s.AllFailures().DeleteAll()
return query.Error() }
func (s *Service) DeleteHits() error {
return s.AllHits().DeleteAll()
}
func (s *Service) DeleteCheckins() error {
for _, c := range s.Checkins() {
if err := c.Delete(); err != nil {
return err
}
}
return nil
}
//func (s *Service) AfterDelete() error {
//
// return nil
//}
func (s *Service) AfterFind() error {
s.UpdateStats()
return nil
} }

View File

@ -15,26 +15,13 @@ func (s *Service) AllFailures() failures.Failurer {
return failures.AllFailures(s) return failures.AllFailures(s)
} }
func (s *Service) LastFailure() *failures.Failure { func (s *Service) FailuresSince(t time.Time) failures.Failurer {
var fail failures.Failure fails := failures.Since(t, s)
failures.DB().Where("service = ?", s.Id).Order("id desc").Limit(1).Find(&fail)
return &fail
}
func (s *Service) FailuresCount() int {
var amount int
failures.DB().Where("service = ?", s.Id).Count(&amount)
return amount
}
func (s *Service) FailuresSince(t time.Time) []*failures.Failure {
var fails []*failures.Failure
failures.DB().Where("service = ?", s.Id).Find(&fails)
return fails return fails
} }
func (s *Service) DowntimeText() string { func (s *Service) DowntimeText() string {
last := s.LastFailure() last := s.AllFailures().Last()
if last == nil { if last == nil {
return "" return ""
} }

View File

@ -22,5 +22,5 @@ func (s *Service) AllHits() hits.Hitters {
} }
func (s *Service) HitsSince(t time.Time) hits.Hitters { func (s *Service) HitsSince(t time.Time) hits.Hitters {
return hits.HitsSince(t, s) return hits.Since(t, s)
} }

View File

@ -4,6 +4,7 @@ import (
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/statping/statping/types"
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"net/url" "net/url"
@ -33,6 +34,13 @@ func (s *Service) Close() {
} }
} }
func humanMicro(val int64) string {
if val < 10000 {
return fmt.Sprintf("%d μs", val)
}
return fmt.Sprintf("%0.0f ms", float64(val)*0.001)
}
// IsRunning returns true if the service go routine is running // IsRunning returns true if the service go routine is running
func (s *Service) IsRunning() bool { func (s *Service) IsRunning() bool {
if s.Running == nil { if s.Running == nil {
@ -68,7 +76,7 @@ func SelectAllServices(start bool) (map[int64]*Service, error) {
CheckinProcess(s) CheckinProcess(s)
} }
fails := s.AllFailures().Last(limitedFailures) fails := s.AllFailures().LastAmount(limitedFailures)
s.Failures = fails s.Failures = fails
for _, c := range s.Checkins() { for _, c := range s.Checkins() {
@ -137,12 +145,12 @@ func (s *Service) UpdateStats() *Service {
s.Online24Hours = s.OnlineDaysPercent(1) s.Online24Hours = s.OnlineDaysPercent(1)
s.Online7Days = s.OnlineDaysPercent(7) s.Online7Days = s.OnlineDaysPercent(7)
s.AvgResponse = s.AvgTime() s.AvgResponse = s.AvgTime()
s.FailuresLast24Hours = len(s.AllFailures().Since(utils.Now().Add(-time.Hour * 24))) s.FailuresLast24Hours = s.FailuresSince(utils.Now().Add(-time.Hour * 24)).Count()
if s.LastOffline.IsZero() { if s.LastOffline.IsZero() {
lastFail := s.LastFailure() lastFail := s.AllFailures().Last()
if lastFail != nil { if lastFail != nil {
s.LastOffline = s.LastFailure().CreatedAt s.LastOffline = lastFail.CreatedAt
} }
} }
@ -155,29 +163,31 @@ func (s *Service) UpdateStats() *Service {
} }
// AvgTime will return the average amount of time for a service to response back successfully // AvgTime will return the average amount of time for a service to response back successfully
func (s *Service) AvgTime() int64 { func (s *Service) AvgTime() float64 {
return s.AllHits().Avg() return s.AllHits().Avg()
} }
// OnlineDaysPercent returns the service's uptime percent within last 24 hours // OnlineDaysPercent returns the service's uptime percent within last 24 hours
func (s *Service) OnlineDaysPercent(days int) float32 { func (s *Service) OnlineDaysPercent(days int) float32 {
ago := utils.Now().Add((-24 * time.Duration(days)) * time.Hour) ago := utils.Now().Add(-time.Duration(days) * types.Day)
return s.OnlineSince(ago) return s.OnlineSince(ago)
} }
// OnlineSince accepts a time since parameter to return the percent of a service's uptime. // OnlineSince accepts a time since parameter to return the percent of a service's uptime.
func (s *Service) OnlineSince(ago time.Time) float32 { func (s *Service) OnlineSince(ago time.Time) float32 {
failed := s.AllFailures().Since(ago) failed := s.FailuresSince(ago)
if len(failed) == 0 { failsList := failed.List()
if len(failsList) == 0 {
s.Online24Hours = 100.00 s.Online24Hours = 100.00
return s.Online24Hours return s.Online24Hours
} }
total := s.AllHits().Since(ago) total := s.HitsSince(ago)
if len(total) == 0 { hitsList := total.List()
if len(hitsList) == 0 {
s.Online24Hours = 0 s.Online24Hours = 0
return s.Online24Hours return s.Online24Hours
} }
avg := float64(len(failed)) / float64(len(total)) * 100 avg := float64(len(hitsList)) / float64(len(failsList)) * 100
avg = 100 - avg avg = 100 - avg
if avg < 0 { if avg < 0 {
avg = 0 avg = 0
@ -190,12 +200,12 @@ func (s *Service) OnlineSince(ago time.Time) float32 {
// Downtime returns the amount of time of a offline service // Downtime returns the amount of time of a offline service
func (s *Service) Downtime() time.Duration { func (s *Service) Downtime() time.Duration {
hit := s.LastHit() hit := s.LastHit()
fail := s.LastFailure() fail := s.AllFailures().Last()
if hit == nil { if hit == nil {
return time.Duration(0) return time.Duration(0)
} }
if fail == nil { if fail == nil {
return utils.Now().Sub(fail.CreatedAt) return utils.Now().Sub(hit.CreatedAt)
} }
return fail.CreatedAt.Sub(hit.CreatedAt) return fail.CreatedAt.Sub(hit.CreatedAt)

View File

@ -64,9 +64,9 @@ func parseHost(s *Service) string {
} }
// dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took // dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took
func dnsCheck(s *Service) (float64, error) { func dnsCheck(s *Service) (int64, error) {
var err error var err error
t1 := time.Now() t1 := utils.Now()
host := parseHost(s) host := parseHost(s)
if s.Type == "tcp" { if s.Type == "tcp" {
_, err = net.LookupHost(host) _, err = net.LookupHost(host)
@ -77,7 +77,7 @@ func dnsCheck(s *Service) (float64, error) {
return 0, err return 0, err
} }
t2 := time.Now() t2 := time.Now()
subTime := t2.Sub(t1).Seconds() subTime := t2.Sub(t1).Microseconds()
return subTime, err return subTime, err
} }
@ -101,7 +101,7 @@ func CheckIcmp(s *Service, record bool) *Service {
} }
p.AddIPAddr(ra) p.AddIPAddr(ra)
p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) { p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) {
s.Latency = rtt.Seconds() s.Latency = rtt.Microseconds()
recordSuccess(s) recordSuccess(s)
} }
err = p.Run() err = p.Run()
@ -125,7 +125,7 @@ func CheckTcp(s *Service, record bool) *Service {
return s return s
} }
s.PingTime = dnsLookup s.PingTime = dnsLookup
t1 := time.Now() t1 := utils.Now()
domain := fmt.Sprintf("%v", s.Domain) domain := fmt.Sprintf("%v", s.Domain)
if s.Port != 0 { if s.Port != 0 {
domain = fmt.Sprintf("%v:%v", s.Domain, s.Port) domain = fmt.Sprintf("%v:%v", s.Domain, s.Port)
@ -146,8 +146,8 @@ func CheckTcp(s *Service, record bool) *Service {
} }
return s return s
} }
t2 := time.Now() t2 := utils.Now()
s.Latency = t2.Sub(t1).Seconds() s.Latency = t2.Sub(t1).Microseconds()
s.LastResponse = "" s.LastResponse = ""
if record { if record {
recordSuccess(s) recordSuccess(s)
@ -171,7 +171,7 @@ func CheckHttp(s *Service, record bool) *Service {
return s return s
} }
s.PingTime = dnsLookup s.PingTime = dnsLookup
t1 := time.Now() t1 := utils.Now()
timeout := time.Duration(s.Timeout) * time.Second timeout := time.Duration(s.Timeout) * time.Second
var content []byte var content []byte
@ -195,8 +195,8 @@ func CheckHttp(s *Service, record bool) *Service {
} }
return s return s
} }
t2 := time.Now() t2 := utils.Now()
s.Latency = t2.Sub(t1).Seconds() s.Latency = t2.Sub(t1).Microseconds()
s.LastResponse = string(content) s.LastResponse = string(content)
s.LastStatusCode = res.StatusCode s.LastStatusCode = res.StatusCode
@ -226,21 +226,21 @@ func CheckHttp(s *Service, record bool) *Service {
// recordSuccess will create a new 'hit' record in the database for a successful/online service // recordSuccess will create a new 'hit' record in the database for a successful/online service
func recordSuccess(s *Service) { func recordSuccess(s *Service) {
s.LastOnline = time.Now().UTC() s.LastOnline = utils.Now()
s.Online = true s.Online = true
hit := &hits.Hit{ hit := &hits.Hit{
Service: s.Id, Service: s.Id,
Latency: s.Latency, Latency: s.Latency,
PingTime: s.PingTime, PingTime: s.PingTime,
CreatedAt: time.Now().UTC(), CreatedAt: utils.Now(),
} }
if err := hit.Create(); err != nil { if err := hit.Create(); err != nil {
log.Error(err) log.Error(err)
} }
log.WithFields(utils.ToFields(hit, s)).Infoln( log.WithFields(utils.ToFields(hit, s)).Infoln(
fmt.Sprintf("Service #%d '%v' Successful Response: %0.2f ms | Lookup in: %0.2f ms | Online: %v | Interval: %d seconds", s.Id, s.Name, hit.Latency*1000, hit.PingTime*1000, s.Online, s.Interval)) fmt.Sprintf("Service #%d '%v' Successful Response: %s | Lookup in: %s | Online: %v | Interval: %d seconds", s.Id, s.Name, humanMicro(hit.Latency), humanMicro(hit.PingTime), s.Online, s.Interval))
s.LastLookupTime = int64(hit.PingTime * 1000) s.LastLookupTime = hit.PingTime
s.LastLatency = int64(hit.Latency * 1000) s.LastLatency = hit.Latency
//notifier.OnSuccess(s) //notifier.OnSuccess(s)
s.SuccessNotified = true s.SuccessNotified = true
} }

View File

@ -0,0 +1,285 @@
package services
import (
"fmt"
"github.com/statping/statping/database"
"github.com/statping/statping/types/checkins"
"github.com/statping/statping/types/failures"
"github.com/statping/statping/types/hits"
"github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
var example = &Service{
Name: "Example Service",
Domain: "https://statping.com",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 5,
Order: 1,
VerifySSL: null.NewNullBool(true),
Public: null.NewNullBool(true),
GroupId: 1,
Permalink: null.NewNullString("statping"),
LastCheck: utils.Now().Add(-5 * time.Second),
LastOffline: utils.Now().Add(-5 * time.Second),
LastOnline: utils.Now().Add(-60 * time.Second),
}
var hit1 = &hits.Hit{
Service: 1,
Latency: 0.1234,
PingTime: 0.01234,
CreatedAt: utils.Now().Add(-120 * time.Second),
}
var hit2 = &hits.Hit{
Service: 1,
Latency: 0.2345,
PingTime: 0.02345,
CreatedAt: utils.Now().Add(-60 * time.Second),
}
var hit3 = &hits.Hit{
Service: 1,
Latency: 0.3456,
PingTime: 0.03456,
CreatedAt: utils.Now().Add(-30 * time.Second),
}
var exmapleCheckin = &checkins.Checkin{
ServiceId: 1,
Name: "Example Checkin",
Interval: 60,
GracePeriod: 30,
ApiKey: "wdededede",
}
var fail1 = &failures.Failure{
Issue: "example not found",
ErrorCode: 404,
Service: 1,
PingTime: 0.0123,
CreatedAt: utils.Now().Add(-160 * time.Second),
}
var fail2 = &failures.Failure{
Issue: "example 2 not found",
ErrorCode: 500,
Service: 1,
PingTime: 0.0123,
CreatedAt: utils.Now().Add(-5 * time.Second),
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.AutoMigrate(&Service{}, &hits.Hit{}, &checkins.Checkin{}, &checkins.CheckinHit{}, &failures.Failure{})
db.Create(&example)
db.Create(&hit1)
db.Create(&hit2)
db.Create(&hit3)
db.Create(&exmapleCheckin)
db.Create(&fail1)
db.Create(&fail2)
checkins.SetDB(db)
failures.SetDB(db)
hits.SetDB(db)
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "Example Service", item.Name)
assert.NotZero(t, item.LastOnline)
assert.NotZero(t, item.LastOffline)
assert.NotZero(t, item.LastCheck)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestService_Checkins(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Len(t, item.Checkins(), 1)
}
func TestService_AllHits(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Len(t, item.AllHits().List(), 3)
assert.Equal(t, 3, item.AllHits().Count())
}
func TestService_AllFailures(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Len(t, item.AllFailures().List(), 2)
assert.Equal(t, 2, item.AllFailures().Count())
}
func TestService_FirstHit(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
hit := item.FirstHit()
assert.Equal(t, int64(1), hit.Id)
}
func TestService_LastHit(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
hit := item.AllHits().Last()
assert.Equal(t, int64(3), hit.Id)
}
func TestService_LastFailure(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
fail := item.AllFailures().Last()
assert.Equal(t, int64(2), fail.Id)
}
func TestService_Duration(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, float64(30), item.Duration().Seconds())
}
func TestService_AvgTime(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "", item.AvgTime())
}
func TestService_HitsSince(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
count := item.HitsSince(utils.Now().Add(-30 * time.Second))
assert.Equal(t, 1, count.Count())
count = item.HitsSince(utils.Now().Add(-180 * time.Second))
assert.Equal(t, 3, count.Count())
}
func TestService_IsRunning(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.False(t, item.IsRunning())
}
func TestService_OnlineDaysPercent(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
amount := item.OnlineDaysPercent(1)
assert.Equal(t, "", amount)
}
func TestService_Downtime(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
amount := item.Downtime().Seconds()
assert.Equal(t, "25", fmt.Sprintf("%0.f", amount))
}
func TestService_FailuresSince(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
count := item.FailuresSince(utils.Now().Add(-6 * time.Second))
assert.Equal(t, 1, count.Count())
count = item.FailuresSince(utils.Now().Add(-180 * time.Second))
assert.Equal(t, 2, count.Count())
}
func TestCreate(t *testing.T) {
example := &Service{
Name: "Example Service 2",
Domain: "https://slack.statping.com",
ExpectedStatus: 200,
Interval: 10,
Type: "http",
Method: "GET",
Timeout: 5,
Order: 3,
VerifySSL: null.NewNullBool(true),
Public: null.NewNullBool(false),
GroupId: 1,
Permalink: null.NewNullString("statping2"),
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "Example Service 2", example.Name)
assert.False(t, example.Public.Bool)
assert.NotZero(t, example.CreatedAt)
assert.Equal(t, int64(2), example.Id)
assert.Len(t, allServices, 2)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Name = "Updated Service"
item.Order = 1
err = item.Update()
require.Nil(t, err)
assert.Equal(t, int64(1), item.Id)
assert.Equal(t, "Updated Service", item.Name)
}
func TestAllInOrder(t *testing.T) {
inOrder := AllInOrder()
assert.Len(t, inOrder, 2)
assert.Equal(t, "Updated Service", inOrder[0].Name)
assert.Equal(t, "Example Service 2", inOrder[1].Name)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, int64(1), item.Id)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestService_CheckService(t *testing.T) {
item, err := Find(2)
require.Nil(t, err)
hitsCount := item.AllHits().Count()
failsCount := item.AllFailures().Count()
assert.Equal(t, 3, hitsCount)
assert.Equal(t, 2, failsCount)
item.CheckService(true)
assert.Equal(t, 4, hitsCount)
assert.Equal(t, 2, failsCount)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}

View File

@ -41,11 +41,11 @@ type Service struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Online bool `gorm:"-" json:"online"` Online bool `gorm:"-" json:"online"`
Latency float64 `gorm:"-" json:"latency"` Latency int64 `gorm:"-" json:"latency"`
PingTime float64 `gorm:"-" json:"ping_time"` PingTime int64 `gorm:"-" json:"ping_time"`
Online24Hours float32 `gorm:"-" json:"online_24_hours"` Online24Hours float32 `gorm:"-" json:"online_24_hours"`
Online7Days float32 `gorm:"-" json:"online_7_days"` Online7Days float32 `gorm:"-" json:"online_7_days"`
AvgResponse int64 `gorm:"-" json:"avg_response"` AvgResponse float64 `gorm:"-" json:"avg_response"`
FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"` FailuresLast24Hours int `gorm:"-" json:"failures_24_hours"`
Running chan bool `gorm:"-" json:"-"` Running chan bool `gorm:"-" json:"-"`
Checkpoint time.Time `gorm:"-" json:"-"` Checkpoint time.Time `gorm:"-" json:"-"`

View File

@ -4,56 +4,56 @@ import (
"github.com/prometheus/common/log" "github.com/prometheus/common/log"
"github.com/statping/statping/database" "github.com/statping/statping/database"
"github.com/statping/statping/utils" "github.com/statping/statping/utils"
"time"
) )
func DB() database.Database { var db database.Database
return database.DB().Model(&User{})
func SetDB(database database.Database) {
db = database.Model(&User{})
} }
func Find(id int64) (*User, error) { func Find(id int64) (*User, error) {
var user User var user User
db := DB().Where("id = ?", id).Find(&user) q := db.Where("id = ?", id).Find(&user)
return &user, db.Error() return &user, q.Error()
} }
func FindByUsername(username string) (*User, error) { func FindByUsername(username string) (*User, error) {
var user User var user User
db := DB().Where("username = ?", username).Find(&user) q := db.Where("username = ?", username).Find(&user)
return &user, db.Error() return &user, q.Error()
} }
func All() []*User { func All() []*User {
var users []*User var users []*User
DB().Find(&users) db.Find(&users)
return users return users
} }
func (u *User) Create() error { func (u *User) Create() error {
u.CreatedAt = time.Now().UTC() q := db.Create(u)
u.Password = utils.HashPassword(u.Password)
if u.ApiKey == "" || u.ApiSecret == "" {
u.ApiKey = utils.NewSHA1Hash(16)
u.ApiSecret = utils.NewSHA1Hash(16)
}
db := DB().Create(u)
if db.Error() == nil { if db.Error() == nil {
log.Warnf("User #%d (%s) has been created", u.Id, u.Username) log.Warnf("User #%d (%s) has been created", u.Id, u.Username)
} }
return db.Error() return q.Error()
} }
func (u *User) Update() error { func (u *User) Update() error {
//u.ApiKey = utils.NewSHA1Hash(5) q := db.Update(u)
//u.ApiSecret = utils.NewSHA1Hash(10) return q.Error()
db := DB().Update(u)
return db.Error()
} }
func (u *User) Delete() error { func (u *User) Delete() error {
db := DB().Delete(u) q := db.Delete(u)
if db.Error() == nil { if db.Error() == nil {
log.Warnf("User #%d (%s) has been deleted", u.Id, u.Username) log.Warnf("User #%d (%s) has been deleted", u.Id, u.Username)
} }
return db.Error() return q.Error()
}
func (u *User) BeforeCreate() error {
u.Password = utils.HashPassword(u.Password)
u.ApiKey = utils.NewSHA1Hash(16)
u.ApiSecret = utils.NewSHA1Hash(16)
return nil
} }

View File

@ -2,7 +2,6 @@ package users
import ( import (
"github.com/statping/statping/types/null" "github.com/statping/statping/types/null"
"github.com/statping/statping/utils"
"time" "time"
) )
@ -18,14 +17,3 @@ type User struct {
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"` CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"` UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
} }
// BeforeCreate for User will set CreatedAt to UTC
func (u *User) BeforeCreate() (err error) {
u.ApiKey = utils.RandomString(16)
u.ApiSecret = utils.RandomString(16)
if u.CreatedAt.IsZero() {
u.CreatedAt = time.Now().UTC()
u.UpdatedAt = time.Now().UTC()
}
return
}

92
types/users/users_test.go Normal file
View File

@ -0,0 +1,92 @@
package users
import (
"github.com/statping/statping/database"
"github.com/statping/statping/types/null"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
)
var example = &User{
Username: "example_user",
Email: "Description here",
Password: "password123",
Admin: null.NewNullBool(true),
}
func TestInit(t *testing.T) {
db, err := database.OpenTester()
require.Nil(t, err)
db.CreateTable(&User{})
db.Create(&example)
SetDB(db)
}
func TestFind(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
assert.Equal(t, "example_user", item.Username)
assert.NotEmpty(t, item.ApiKey)
assert.NotEmpty(t, item.ApiSecret)
assert.NotEqual(t, "password123", item.Password)
assert.True(t, item.Admin.Bool)
}
func TestFindByUsername(t *testing.T) {
item, err := FindByUsername("example_user")
require.Nil(t, err)
assert.Equal(t, "example_user", item.Username)
assert.NotEmpty(t, item.ApiKey)
assert.NotEmpty(t, item.ApiSecret)
assert.NotEqual(t, "password123", item.Password)
assert.True(t, item.Admin.Bool)
}
func TestAll(t *testing.T) {
items := All()
assert.Len(t, items, 1)
}
func TestCreate(t *testing.T) {
example := &User{
Username: "exampleuser2",
Password: "password12345",
Email: "info@yahoo.com",
}
err := example.Create()
require.Nil(t, err)
assert.NotZero(t, example.Id)
assert.Equal(t, "exampleuser2", example.Username)
assert.NotEqual(t, "password12345", example.Password)
assert.NotZero(t, example.CreatedAt)
assert.NotEmpty(t, example.ApiKey)
assert.NotEmpty(t, example.ApiSecret)
}
func TestUpdate(t *testing.T) {
item, err := Find(1)
require.Nil(t, err)
item.Username = "updated_user"
err = item.Update()
require.Nil(t, err)
assert.Equal(t, "updated_user", item.Username)
}
func TestDelete(t *testing.T) {
all := All()
assert.Len(t, all, 2)
item, err := Find(1)
require.Nil(t, err)
err = item.Delete()
require.Nil(t, err)
all = All()
assert.Len(t, all, 1)
}
func TestClose(t *testing.T) {
assert.Nil(t, db.Close())
}