pull/429/head
hunterlong 2020-02-25 21:38:03 -08:00
parent 7ac26c2841
commit 5e1b92a313
48 changed files with 937 additions and 908 deletions

View File

@ -21,7 +21,6 @@ import (
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers"
"github.com/hunterlong/statping/plugin"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
@ -72,13 +71,6 @@ func catchCLI(args []string) error {
case "update":
updateDisplay()
return errors.New("end")
case "test":
cmd := args[1]
switch cmd {
case "plugins":
plugin.LoadPlugins()
}
return errors.New("end")
case "static":
var err error
if err = runLogs(); err != nil {
@ -88,7 +80,7 @@ func catchCLI(args []string) error {
return err
}
fmt.Printf("Statping v%v Exporting Static 'index.html' page...\n", VERSION)
if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil {
if _, err = core.LoadConfigFile(dir); err != nil {
log.Errorln("config.yml file not found")
return err
}
@ -111,7 +103,7 @@ func catchCLI(args []string) error {
if err = runAssets(); err != nil {
return err
}
if core.CoreApp.Config, err = core.LoadConfigFile(dir); err != nil {
if _, err = core.LoadConfigFile(dir); err != nil {
return err
}
if err = core.CoreApp.Connect(false, dir); err != nil {
@ -207,7 +199,7 @@ func updateDisplay() error {
// runOnce will initialize the Statping application and check each service 1 time, will not run HTTP server
func runOnce() {
var err error
core.CoreApp.Config, err = core.LoadConfigFile(utils.Directory)
_, err = core.LoadConfigFile(utils.Directory)
if err != nil {
log.Errorln("config.yml file not found")
}

View File

@ -84,6 +84,7 @@ func TestExportCommand(t *testing.T) {
}
func TestUpdateCommand(t *testing.T) {
t.SkipNow()
cmd := helperCommand(nil, "version")
var got = make(chan string)
commandAndSleep(cmd, time.Duration(15*time.Second), got)
@ -93,6 +94,7 @@ func TestUpdateCommand(t *testing.T) {
}
func TestAssetsCommand(t *testing.T) {
t.SkipNow()
c := testcli.Command("statping", "assets")
c.Run()
t.Log(c.Stdout())

View File

@ -22,7 +22,6 @@ import (
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/handlers"
"github.com/hunterlong/statping/plugin"
"github.com/hunterlong/statping/source"
"github.com/joho/godotenv"
"os"
@ -49,22 +48,17 @@ func init() {
// parseFlags will parse the application flags
// -ip = 0.0.0.0 IP address for outgoing HTTP server
// -port = 8080 Port number for outgoing HTTP server
// environment variables WILL overwrite flags
func parseFlags() {
flag.StringVar(&ipAddress, "ip", "0.0.0.0", "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", 8080, "Port to run the HTTP server")
flag.IntVar(&verboseMode, "verbose", 2, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
envPort := utils.Getenv("PORT", 8080).(int)
envIpAddress := utils.Getenv("IP", "0.0.0.0").(string)
envVerbose := utils.Getenv("VERBOSE", 2).(int)
if os.Getenv("PORT") != "" {
port = int(utils.ToInt(os.Getenv("PORT")))
}
if os.Getenv("IP") != "" {
ipAddress = os.Getenv("IP")
}
if os.Getenv("VERBOSE") != "" {
verboseMode = int(utils.ToInt(os.Getenv("VERBOSE")))
}
flag.StringVar(&ipAddress, "ip", envIpAddress, "IP address to run the Statping HTTP server")
flag.StringVar(&envFile, "env", "", "IP address to run the Statping HTTP server")
flag.IntVar(&port, "port", envPort, "Port to run the HTTP server")
flag.IntVar(&verboseMode, "verbose", envVerbose, "Run in verbose mode to see detailed logs (1 - 4)")
flag.Parse()
}
// main will run the Statping application
@ -93,7 +87,7 @@ func main() {
log.Info(fmt.Sprintf("Starting Statping v%v", VERSION))
updateDisplay()
configs, err := core.LoadConfigFile(utils.Directory)
_, err = core.LoadConfigFile(utils.Directory)
if err != nil {
log.Errorln(err)
core.CoreApp.Setup = false
@ -108,16 +102,16 @@ func main() {
log.Fatalln(err)
}
}
core.CoreApp.Config = configs
if err := mainProcess(); err != nil {
log.Fatalln(err)
os.Exit(2)
}
}
// Close will gracefully stop the database connection, and log file
func Close() {
core.CloseDB()
utils.CloseLogs()
core.CloseDB()
}
// sigterm will attempt to close the database connections gracefully
@ -147,10 +141,13 @@ func mainProcess() error {
log.Errorln(fmt.Sprintf("could not connect to database: %v", err))
return err
}
core.CoreApp.MigrateDatabase()
core.InitApp()
if err := core.CoreApp.MigrateDatabase(); err != nil {
return err
}
if err := core.InitApp(); err != nil {
return err
}
if core.CoreApp.Setup {
plugin.LoadPlugins()
if err := handlers.RunHTTPServer(ipAddress, port); err != nil {
log.Fatalln(err)
}

View File

@ -34,16 +34,17 @@ import (
// checkServices will start the checking go routine for each service
func checkServices() {
log.Infoln(fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.services)))
for _, ser := range CoreApp.services {
for _, s := range CoreApp.services {
//go CheckinRoutine()
go ServiceCheckQueue(ser, true)
time.Sleep(200 * time.Millisecond) // short delay so requests don't run all at the same time.
go ServiceCheckQueue(s, true)
}
}
// Check will run checkHttp for HTTP services and checkTcp for TCP services
// if record param is set to true, it will add a record into the database.
func CheckService(srv database.Servicer, record bool) {
switch srv.Model().Type {
func CheckService(srv *Service, record bool) {
switch srv.Type {
case "http":
CheckHttp(srv, record)
case "tcp", "udp":
@ -54,8 +55,7 @@ func CheckService(srv database.Servicer, record bool) {
}
// CheckQueue is the main go routine for checking a service
func ServiceCheckQueue(srv database.Servicer, record bool) {
s := srv.Model()
func ServiceCheckQueue(s *Service, record bool) {
s.Checkpoint = time.Now()
s.SleepDuration = (time.Duration(s.Id) * 100) * time.Millisecond
CheckLoop:
@ -65,11 +65,11 @@ CheckLoop:
log.Infoln(fmt.Sprintf("Stopping service: %v", s.Name))
break CheckLoop
case <-time.After(s.SleepDuration):
CheckService(srv, record)
s.Checkpoint = s.Checkpoint.Add(srv.Interval())
CheckService(s, record)
s.Checkpoint = s.Checkpoint.Add(s.Duration())
sleep := s.Checkpoint.Sub(time.Now())
if !s.Online {
s.SleepDuration = srv.Interval()
s.SleepDuration = s.Duration()
} else {
s.SleepDuration = sleep
}
@ -78,19 +78,7 @@ CheckLoop:
}
}
// duration returns the amount of duration for a service to check its status
func duration(s database.Servicer) time.Duration {
var amount time.Duration
if s.Interval() >= 10000 {
amount = s.Interval() * time.Microsecond
} else {
amount = s.Interval() * time.Second
}
return amount
}
func parseHost(srv database.Servicer) string {
s := srv.Model()
func parseHost(s *Service) string {
if s.Type == "tcp" || s.Type == "udp" {
return s.Domain
} else {
@ -103,11 +91,10 @@ func parseHost(srv database.Servicer) string {
}
// dnsCheck will check the domain name and return a float64 for the amount of time the DNS check took
func dnsCheck(srv database.Servicer) (float64, error) {
s := srv.Model()
func dnsCheck(s *Service) (float64, error) {
var err error
t1 := time.Now()
host := parseHost(srv)
host := parseHost(s)
if s.Type == "tcp" {
_, err = net.LookupHost(host)
} else {
@ -126,8 +113,7 @@ func isIPv6(address string) bool {
}
// checkIcmp will send a ICMP ping packet to the service
func CheckIcmp(srv database.Servicer, record bool) *types.Service {
s := srv.Model()
func CheckIcmp(s *Service, record bool) *types.Service {
p := fastping.NewPinger()
resolveIP := "ip4:icmp"
if isIPv6(s.Domain) {
@ -135,8 +121,8 @@ func CheckIcmp(srv database.Servicer, record bool) *types.Service {
}
ra, err := net.ResolveIPAddr(resolveIP, s.Domain)
if err != nil {
recordFailure(srv, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
return s
recordFailure(s, fmt.Sprintf("Could not send ICMP to service %v, %v", s.Domain, err))
return s.Service
}
p.AddIPAddr(ra)
p.OnRecv = func(addr *net.IPAddr, rtt time.Duration) {
@ -145,22 +131,21 @@ func CheckIcmp(srv database.Servicer, record bool) *types.Service {
}
err = p.Run()
if err != nil {
recordFailure(srv, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err))
return s
recordFailure(s, fmt.Sprintf("Issue running ICMP to service %v, %v", s.Domain, err))
return s.Service
}
s.LastResponse = ""
return s
return s.Service
}
// checkTcp will check a TCP service
func CheckTcp(srv database.Servicer, record bool) *types.Service {
s := srv.Model()
dnsLookup, err := dnsCheck(srv)
func CheckTcp(s *Service, record bool) *types.Service {
dnsLookup, err := dnsCheck(s)
if err != nil {
if record {
recordFailure(srv, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err))
recordFailure(s, fmt.Sprintf("Could not get IP address for TCP service %v, %v", s.Domain, err))
}
return s
return s.Service
}
s.PingTime = dnsLookup
t1 := time.Now()
@ -174,15 +159,15 @@ func CheckTcp(srv database.Servicer, record bool) *types.Service {
conn, err := net.DialTimeout(s.Type, domain, time.Duration(s.Timeout)*time.Second)
if err != nil {
if record {
recordFailure(srv, fmt.Sprintf("Dial Error %v", err))
recordFailure(s, fmt.Sprintf("Dial Error %v", err))
}
return s
return s.Service
}
if err := conn.Close(); err != nil {
if record {
recordFailure(srv, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err))
recordFailure(s, fmt.Sprintf("%v Socket Close Error %v", strings.ToUpper(s.Type), err))
}
return s
return s.Service
}
t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds()
@ -190,18 +175,17 @@ func CheckTcp(srv database.Servicer, record bool) *types.Service {
if record {
recordSuccess(s)
}
return s
return s.Service
}
// checkHttp will check a HTTP service
func CheckHttp(srv database.Servicer, record bool) *types.Service {
s := srv.Model()
dnsLookup, err := dnsCheck(srv)
func CheckHttp(s *Service, record bool) *types.Service {
dnsLookup, err := dnsCheck(s)
if err != nil {
if record {
recordFailure(srv, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
recordFailure(s, fmt.Sprintf("Could not get IP address for domain %v, %v", s.Domain, err))
}
return s
return s.Service
}
s.PingTime = dnsLookup
t1 := time.Now()
@ -224,9 +208,9 @@ func CheckHttp(srv database.Servicer, record bool) *types.Service {
}
if err != nil {
if record {
recordFailure(srv, fmt.Sprintf("HTTP Error %v", err))
recordFailure(s, fmt.Sprintf("HTTP Error %v", err))
}
return s
return s.Service
}
t2 := time.Now()
s.Latency = t2.Sub(t1).Seconds()
@ -240,25 +224,25 @@ func CheckHttp(srv database.Servicer, record bool) *types.Service {
}
if !match {
if record {
recordFailure(srv, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected))
recordFailure(s, fmt.Sprintf("HTTP Response Body did not match '%v'", s.Expected))
}
return s
return s.Service
}
}
if s.ExpectedStatus != res.StatusCode {
if record {
recordFailure(srv, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus))
recordFailure(s, fmt.Sprintf("HTTP Status Code %v did not match %v", res.StatusCode, s.ExpectedStatus))
}
return s
return s.Service
}
if record {
recordSuccess(s)
}
return s
return s.Service
}
// recordSuccess will create a new 'hit' record in the database for a successful/online service
func recordSuccess(s *types.Service) {
func recordSuccess(s *Service) {
s.LastOnline = time.Now().UTC()
hit := &types.Hit{
Service: s.Id,
@ -268,14 +252,13 @@ func recordSuccess(s *types.Service) {
}
database.Create(hit)
log.WithFields(utils.ToFields(hit, s)).Infoln(fmt.Sprintf("Service %v Successful Response: %0.2f ms | Lookup in: %0.2f ms", s.Name, hit.Latency*1000, hit.PingTime*1000))
notifier.OnSuccess(s)
notifier.OnSuccess(s.Service)
s.Online = true
s.SuccessNotified = true
}
// recordFailure will create a new 'Failure' record in the database for a offline service
func recordFailure(srv database.Servicer, issue string) {
s := srv.Model()
func recordFailure(s *Service, issue string) {
fail := &types.Failure{
Service: s.Id,
Issue: issue,
@ -288,6 +271,6 @@ func recordFailure(srv database.Servicer, issue string) {
database.Create(fail)
s.Online = false
s.SuccessNotified = false
s.DownText = srv.DowntimeText()
notifier.OnFailure(s, fail)
s.DownText = s.DowntimeText()
notifier.OnFailure(s.Service, fail)
}

View File

@ -99,7 +99,7 @@ func AllCheckins() []*database.CheckinObj {
// SelectCheckin will find a Checkin based on the API supplied
func SelectCheckin(api string) *Checkin {
for _, s := range Services() {
for _, c := range s.AllCheckins() {
for _, c := range s.Checkins() {
if c.ApiKey == api {
return &Checkin{c}
}
@ -140,19 +140,17 @@ func (c *Checkin) GetFailures(count int) []*types.Failure {
}
// Create will create a new Checkin
func (c *Checkin) Delete() error {
func (c *Checkin) Delete() {
c.Close()
i := c.index()
service := c.Service()
slice := service.Checkins
service.Checkins = append(slice[:i], slice[i+1:]...)
row := Database(c).Delete(&c)
return row.Error()
srv := c.Service()
slice := srv.Service.Checkins
srv.Service.Checkins = append(slice[:i], slice[i+1:]...)
}
// index returns a checkin index int for updating the *checkin.Service slice
func (c *Checkin) index() int {
for k, checkin := range c.Service().Checkins {
for k, checkin := range c.Service().Checkins() {
if c.Id == checkin.Model().Id {
return k
}

View File

@ -23,7 +23,6 @@ import (
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"io/ioutil"
"os"
)
// ErrorResponse is used for HTTP errors to show to User
@ -32,13 +31,16 @@ type ErrorResponse struct {
}
// LoadConfigFile will attempt to load the 'config.yml' file in a specific directory
func LoadConfigFile(directory string) (*types.DbConfig, error) {
var configs *types.DbConfig
if os.Getenv("DB_CONN") != "" {
log.Warnln("DB_CONN environment variable was found, waiting for database...")
func LoadConfigFile(directory string) (*DbConfig, error) {
var configs *DbConfig
dbConn := utils.Getenv("DB_CONN", "")
if dbConn != "" {
log.Infof("DB_CONN=%s environment variable was found, waiting for database...", dbConn)
return LoadUsingEnv()
}
log.Debugln("attempting to read config file at: " + directory + "/config.yml")
log.Debugln("Attempting to read config file at: " + directory + "/config.yml")
file, err := ioutil.ReadFile(directory + "/config.yml")
if err != nil {
CoreApp.Setup = false
@ -49,54 +51,49 @@ func LoadConfigFile(directory string) (*types.DbConfig, error) {
return nil, err
}
log.WithFields(utils.ToFields(configs)).Debugln("read config file: " + directory + "/config.yml")
CoreApp.Config = configs
CoreApp.Config = configs.DbConfig
return configs, err
}
// LoadUsingEnv will attempt to load database configs based on environment variables. If DB_CONN is set if will force this function.
func LoadUsingEnv() (*types.DbConfig, error) {
func LoadUsingEnv() (*DbConfig, error) {
Configs, err := EnvToConfig()
if err != nil {
return Configs, err
}
CoreApp.Name = os.Getenv("NAME")
if Configs.Domain == "" {
CoreApp.Domain = Configs.LocalIP
} else {
CoreApp.Domain = os.Getenv("DOMAIN")
}
CoreApp.UseCdn = types.NewNullBool(os.Getenv("USE_CDN") == "true")
CoreApp.Name = utils.Getenv("NAME", "").(string)
CoreApp.Domain = utils.Getenv("DOMAIN", Configs.LocalIP).(string)
CoreApp.UseCdn = types.NewNullBool(utils.Getenv("USE_CDN", false).(bool))
err = CoreApp.Connect(true, utils.Directory)
if err != nil {
log.Errorln(err)
return nil, err
}
if _, err := CoreApp.SaveConfig(Configs); err != nil {
if err := Configs.Save(); err != nil {
return nil, err
}
exists := DbSession.HasTable("core")
if !exists {
log.Infoln(fmt.Sprintf("Core database does not exist, creating now!"))
CoreApp.DropDatabase()
CoreApp.CreateDatabase()
CoreApp, err = CoreApp.InsertCore(Configs)
if err := CoreApp.DropDatabase(); err != nil {
return nil, err
}
if err := CoreApp.CreateDatabase(); err != nil {
return nil, err
}
CoreApp, err = Configs.InsertCore()
if err != nil {
log.Errorln(err)
}
username := os.Getenv("ADMIN_USER")
if username == "" {
username = "admin"
}
password := os.Getenv("ADMIN_PASSWORD")
if password == "" {
password = "admin"
}
username := utils.Getenv("ADMIN_USER", "admin").(string)
password := utils.Getenv("ADMIN_PASSWORD", "admin").(string)
admin := &types.User{
Username: username,
Password: password,
Password: utils.HashPassword(password),
Email: "info@admin.com",
Admin: types.NewNullBool(true),
}
@ -128,67 +125,56 @@ func defaultPort(db string) int64 {
}
// EnvToConfig converts environment variables to a DbConfig variable
func EnvToConfig() (*types.DbConfig, error) {
func EnvToConfig() (*DbConfig, error) {
var err error
if os.Getenv("DB_CONN") == "" {
return nil, errors.New("Missing DB_CONN environment variable")
}
if os.Getenv("DB_CONN") != "sqlite" {
if os.Getenv("DB_HOST") == "" {
dbConn := utils.Getenv("DB_CONN", "").(string)
dbHost := utils.Getenv("DB_HOST", "").(string)
dbUser := utils.Getenv("DB_USER", "").(string)
dbPass := utils.Getenv("DB_PASS", "").(string)
dbData := utils.Getenv("DB_DATABASE", "").(string)
dbPort := utils.Getenv("DB_PORT", defaultPort(dbConn)).(int64)
name := utils.Getenv("NAME", "Statping").(string)
desc := utils.Getenv("DESCRIPTION", "Statping Monitoring Sample Data").(string)
user := utils.Getenv("ADMIN_USER", "admin").(string)
password := utils.Getenv("ADMIN_PASS", "admin").(string)
domain := utils.Getenv("DOMAIN", "").(string)
sqlFile := utils.Getenv("SQL_FILE", "").(string)
if dbConn != "sqlite" {
if dbHost == "" {
return nil, errors.New("Missing DB_HOST environment variable")
}
if os.Getenv("DB_USER") == "" {
if dbUser == "" {
return nil, errors.New("Missing DB_USER environment variable")
}
if os.Getenv("DB_PASS") == "" {
if dbPass == "" {
return nil, errors.New("Missing DB_PASS environment variable")
}
if os.Getenv("DB_DATABASE") == "" {
if dbData == "" {
return nil, errors.New("Missing DB_DATABASE environment variable")
}
}
port := utils.ToInt(os.Getenv("DB_PORT"))
if port == 0 {
port = defaultPort(os.Getenv("DB_PORT"))
}
name := os.Getenv("NAME")
if name == "" {
name = "Statping"
}
description := os.Getenv("DESCRIPTION")
if description == "" {
description = "Statping Monitoring Sample Data"
}
adminUser := os.Getenv("ADMIN_USER")
if adminUser == "" {
adminUser = "admin"
}
adminPass := os.Getenv("ADMIN_PASS")
if adminPass == "" {
adminPass = "admin"
}
configs := &types.DbConfig{
DbConn: os.Getenv("DB_CONN"),
DbHost: os.Getenv("DB_HOST"),
DbUser: os.Getenv("DB_USER"),
DbPass: os.Getenv("DB_PASS"),
DbData: os.Getenv("DB_DATABASE"),
DbPort: port,
CoreApp.Config = &types.DbConfig{
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
DbPass: dbPass,
DbData: dbData,
DbPort: dbPort,
Project: name,
Description: description,
Domain: os.Getenv("DOMAIN"),
Description: desc,
Domain: domain,
Email: "",
Username: adminUser,
Password: adminPass,
Username: user,
Password: password,
Error: nil,
Location: utils.Directory,
SqlFile: os.Getenv("SQL_FILE"),
SqlFile: sqlFile,
}
CoreApp.Config = configs
return configs, err
return &DbConfig{CoreApp.Config}, err
}
// SampleData runs all the sample data for a new Statping installation

View File

@ -35,7 +35,7 @@ type PluginRepos types.PluginRepos
type Core struct {
*types.Core
services []database.Servicer
services []*Service
}
var (
@ -63,18 +63,31 @@ func (c *Core) ToCore() *types.Core {
}
// InitApp will initialize Statping
func InitApp() {
SelectCore()
InsertNotifierDB()
InsertIntegratorDB()
SelectAllServices(true)
func InitApp() error {
if _, err := SelectCore(); err != nil {
return err
}
if err := InsertNotifierDB(); err != nil {
return err
}
if err := InsertIntegratorDB(); err != nil {
return err
}
if _, err := SelectAllServices(true); err != nil {
return err
}
checkServices()
AttachNotifiers()
AddIntegrations()
if err := AttachNotifiers(); err != nil {
return err
}
if err := AddIntegrations(); err != nil {
return err
}
CoreApp.Notifications = notifier.AllCommunications
CoreApp.Integrations = integrations.Integrations
go DatabaseMaintence()
database.StartMaintenceRoutine()
CoreApp.Setup = true
return nil
}
// InsertNotifierDB inject the Statping database instance to the Notifier package
@ -214,9 +227,9 @@ func AddIntegrations() error {
}
// ServiceOrder will reorder the services based on 'order_id' (Order)
type ServiceOrder []database.Servicer
type ServiceOrder []*Service
// Sort interface for resroting the Services in order
func (c ServiceOrder) Len() int { return len(c) }
func (c ServiceOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Model().Order < c[j].Model().Order }
func (c ServiceOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }

View File

@ -17,6 +17,7 @@ package core
import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -39,26 +40,27 @@ func init() {
func TestNewCore(t *testing.T) {
err := TmpRecords("core.db")
t.Log(err)
require.Nil(t, err)
require.NotNil(t, CoreApp)
}
func TestDbConfig_Save(t *testing.T) {
if skipNewDb {
t.SkipNow()
//if skipNewDb {
// t.SkipNow()
//}
//var err error
//Configs = &DbConfig{
// DbConn: "sqlite",
// Project: "Tester",
// Location: dir,
//}
//Configs, err = Configs.Save()
//assert.Nil(t, err)
//assert.Equal(t, "sqlite", Configs.DbConn)
//assert.NotEmpty(t, Configs.ApiKey)
//assert.NotEmpty(t, Configs.ApiSecret)
}
config := &DbConfig{&types.DbConfig{
DbConn: "sqlite",
Project: "Tester",
Location: dir,
}}
err := config.Save()
require.Nil(t, err)
assert.Equal(t, "sqlite", CoreApp.Config.DbConn)
assert.NotEmpty(t, CoreApp.Config.ApiKey)
assert.NotEmpty(t, CoreApp.Config.ApiSecret)
}
func TestLoadDbConfig(t *testing.T) {
@ -73,7 +75,6 @@ func TestDbConnection(t *testing.T) {
}
func TestDropDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
@ -82,7 +83,6 @@ func TestDropDatabase(t *testing.T) {
}
func TestSeedSchemaDatabase(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}
@ -97,7 +97,6 @@ func TestMigrateDatabase(t *testing.T) {
}
func TestSeedDatabase(t *testing.T) {
t.SkipNow()
err := InsertLargeSampleData()
assert.Nil(t, err)
}
@ -115,7 +114,6 @@ func TestSelectCore(t *testing.T) {
}
func TestInsertNotifierDB(t *testing.T) {
t.SkipNow()
if skipNewDb {
t.SkipNow()
}

View File

@ -39,14 +39,12 @@ var (
func init() {
DbModels = []interface{}{&types.Service{}, &types.User{}, &types.Hit{}, &types.Failure{}, &types.Message{}, &types.Group{}, &types.Checkin{}, &types.CheckinHit{}, &notifier.Notification{}, &types.Incident{}, &types.IncidentUpdate{}, &types.Integration{}}
gorm.NowFunc = func() time.Time {
return time.Now().UTC()
}
}
// DbConfig stores the config.yml file for the statup configuration
type DbConfig types.DbConfig
type DbConfig struct {
*types.DbConfig
}
func Database(obj interface{}) database.Database {
switch obj.(type) {
@ -140,18 +138,18 @@ func CloseDB() {
//}
// InsertCore create the single row for the Core settings in Statping
func (c *Core) InsertCore(db *types.DbConfig) (*Core, error) {
func (d *DbConfig) InsertCore() (*Core, error) {
CoreApp = &Core{Core: &types.Core{
Name: db.Project,
Description: db.Description,
Name: d.Project,
Description: d.Description,
ConfigFile: "config.yml",
ApiKey: utils.NewSHA1Hash(9),
ApiSecret: utils.NewSHA1Hash(16),
Domain: db.Domain,
Domain: d.Domain,
MigrationId: time.Now().Unix(),
Config: db,
Config: d.DbConfig,
}}
query := Database(CoreApp).Create(&CoreApp)
query := DbSession.Create(CoreApp.Core)
return CoreApp, query.Error()
}
@ -219,9 +217,13 @@ func (c *Core) Connect(retry bool, location string) error {
}
log.WithFields(utils.ToFields(dbSession)).Debugln("connected to database")
dbSession.DB().SetMaxOpenConns(5)
dbSession.DB().SetMaxIdleConns(5)
dbSession.DB().SetConnMaxLifetime(1 * time.Minute)
maxOpenConn := utils.Getenv("MAX_OPEN_CONN", 5)
maxIdleConn := utils.Getenv("MAX_IDLE_CONN", 5)
maxLifeConn := utils.Getenv("MAX_LIFE_CONN", 2*time.Minute)
dbSession.DB().SetMaxOpenConns(maxOpenConn.(int))
dbSession.DB().SetMaxIdleConns(maxIdleConn.(int))
dbSession.DB().SetConnMaxLifetime(maxLifeConn.(time.Duration))
if dbSession.DB().Ping() == nil {
DbSession = dbSession
@ -239,26 +241,6 @@ func (c *Core) waitForDb() error {
return c.Connect(true, utils.Directory)
}
// DatabaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func DatabaseMaintence() {
for range time.Tick(60 * time.Minute) {
log.Infoln("Checking for database records older than 3 months...")
since := time.Now().AddDate(0, -3, 0).UTC()
DeleteAllSince("failures", since)
DeleteAllSince("hits", since)
}
}
// DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, date.Format("2006-01-02"))
db := DbSession.Exec(sql)
if db.Error() != nil {
log.Warnln(db.Error())
}
}
// Update will save the config.yml file
func (c *Core) UpdateConfig() error {
var err error
@ -278,25 +260,25 @@ func (c *Core) UpdateConfig() error {
}
// Save will initially create the config.yml file
func (c *Core) SaveConfig(configs *types.DbConfig) (*types.DbConfig, error) {
func (d *DbConfig) Save() error {
config, err := os.Create(utils.Directory + "/config.yml")
if err != nil {
log.Errorln(err)
return nil, err
return err
}
defer config.Close()
log.WithFields(utils.ToFields(configs)).Debugln("saving config file at: " + utils.Directory + "/config.yml")
c.Config = configs
c.Config.ApiKey = utils.NewSHA1Hash(16)
c.Config.ApiSecret = utils.NewSHA1Hash(16)
data, err := yaml.Marshal(configs)
log.WithFields(utils.ToFields(d)).Debugln("saving config file at: " + utils.Directory + "/config.yml")
CoreApp.Config = d.DbConfig
CoreApp.Config.ApiKey = utils.NewSHA1Hash(16)
CoreApp.Config.ApiSecret = utils.NewSHA1Hash(16)
data, err := yaml.Marshal(d)
if err != nil {
log.Errorln(err)
return nil, err
return err
}
config.WriteString(string(data))
log.WithFields(utils.ToFields(configs)).Infoln("saved config file at: " + utils.Directory + "/config.yml")
return c.Config, err
log.WithFields(utils.ToFields(d)).Infoln("saved config file at: " + utils.Directory + "/config.yml")
return err
}
// CreateCore will initialize the global variable 'CoreApp". This global variable contains most of Statping app.
@ -324,18 +306,13 @@ func (c *Core) CreateCore() *Core {
// DropDatabase will DROP each table Statping created
func (c *Core) DropDatabase() error {
log.Infoln("Dropping Database Tables...")
err := DbSession.DropTableIfExists("checkins")
err = DbSession.DropTableIfExists("checkin_hits")
err = DbSession.DropTableIfExists("notifications")
err = DbSession.DropTableIfExists("core")
err = DbSession.DropTableIfExists("failures")
err = DbSession.DropTableIfExists("hits")
err = DbSession.DropTableIfExists("services")
err = DbSession.DropTableIfExists("users")
err = DbSession.DropTableIfExists("messages")
err = DbSession.DropTableIfExists("incidents")
err = DbSession.DropTableIfExists("incident_updates")
tables := []string{"checkins", "checkin_hits", "notifications", "core", "failures", "hits", "services", "users", "messages", "incidents", "incident_updates"}
for _, t := range tables {
if err := DbSession.DropTableIfExists(t); err != nil {
return err.Error()
}
}
return nil
}
// CreateDatabase will CREATE TABLES for each of the Statping elements

View File

@ -19,7 +19,9 @@ import (
"github.com/hunterlong/statping/types"
)
type Failure struct{}
type Failure struct {
*types.Failure
}
const (
limitedFailures = 32

View File

@ -7,46 +7,45 @@ import (
)
type Group struct {
database.Grouper
*types.Group
}
// SelectGroups returns all groups
func SelectGroups(includeAll bool, auth bool) []database.Grouper {
var validGroups []database.Grouper
func SelectGroups(includeAll bool, auth bool) []*Group {
var validGroups []*Group
groups := database.AllGroups()
for _, g := range groups {
if !g.Model().Public.Bool {
if !g.Public.Bool {
if auth {
validGroups = append(validGroups, g)
validGroups = append(validGroups, &Group{g.Group})
}
} else {
validGroups = append(validGroups, g)
validGroups = append(validGroups, &Group{g.Group})
}
}
sort.Sort(GroupOrder(validGroups))
if includeAll {
emptyGroup := &Group{}
validGroups = append(validGroups, emptyGroup)
validGroups = append(validGroups, &Group{})
}
return validGroups
}
// SelectGroup returns a *core.Group
func SelectGroup(id int64) *types.Group {
func SelectGroup(id int64) *Group {
for _, g := range SelectGroups(true, true) {
if g.Model().Id == id {
return g.Model()
if g.Id == id {
return g
}
}
return nil
}
// GroupOrder will reorder the groups based on 'order_id' (Order)
type GroupOrder []database.Grouper
type GroupOrder []*Group
// Sort interface for resorting the Groups in order
func (c GroupOrder) Len() int { return len(c) }
func (c GroupOrder) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
func (c GroupOrder) Less(i, j int) bool { return c[i].Model().Order < c[j].Model().Order }
func (c GroupOrder) Less(i, j int) bool { return c[i].Order < c[j].Order }

View File

@ -17,6 +17,7 @@ package notifier
import (
"fmt"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
_ "github.com/jinzhu/gorm/dialects/sqlite"
@ -57,7 +58,7 @@ var core = &types.Core{
func injectDatabase() {
sqlPath := dir + "/notifier.db"
utils.DeleteFile(sqlPath)
db, _ = types.Openw("sqlite3", sqlPath)
db, _ = database.Openw("sqlite3", sqlPath)
db.CreateTable(&Notification{})
}

View File

@ -34,7 +34,10 @@ var (
func InsertSampleData() error {
log.Infoln("Inserting Sample Data...")
insertSampleGroups()
if err := insertSampleGroups(); err != nil {
return err
}
createdOn := time.Now().Add(((-24 * 30) * 3) * time.Hour).UTC()
s1 := &types.Service{
Name: "Google",
@ -106,15 +109,33 @@ func InsertSampleData() error {
CreatedAt: createdOn,
}
database.Create(s1)
database.Create(s2)
database.Create(s3)
database.Create(s4)
database.Create(s5)
if _, err := database.Create(s1); err != nil {
return types.ErrWrap(err, types.ErrorCreateService)
}
if _, err := database.Create(s2); err != nil {
return types.ErrWrap(err, types.ErrorCreateService)
}
if _, err := database.Create(s3); err != nil {
return types.ErrWrap(err, types.ErrorCreateService)
}
if _, err := database.Create(s4); err != nil {
return types.ErrWrap(err, types.ErrorCreateService)
}
if _, err := database.Create(s5); err != nil {
return types.ErrWrap(err, types.ErrorCreateService)
}
insertMessages()
if _, err := SelectAllServices(false); err != nil {
return types.ErrWrap(err, types.ErrorServiceSelection)
}
insertSampleIncidents()
if err := insertMessages(); err != nil {
return types.ErrWrap(err, types.ErrorCreateMessage)
}
if err := insertSampleIncidents(); err != nil {
return types.ErrWrap(err, types.ErrorCreateIncident)
}
log.Infoln("Sample data has finished importing")
@ -128,7 +149,7 @@ func insertSampleIncidents() error {
ServiceId: 2,
}
if _, err := database.Create(incident1); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateIncidentUp)
}
incidentUpdate1 := &types.IncidentUpdate{
@ -137,7 +158,7 @@ func insertSampleIncidents() error {
Type: "Investigating",
}
if _, err := database.Create(incidentUpdate1); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateIncidentUp)
}
incidentUpdate2 := &types.IncidentUpdate{
@ -146,7 +167,7 @@ func insertSampleIncidents() error {
Type: "Update",
}
if _, err := database.Create(incidentUpdate2); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateIncidentUp)
}
incidentUpdate3 := &types.IncidentUpdate{
@ -155,7 +176,7 @@ func insertSampleIncidents() error {
Type: "Resolved",
}
if _, err := database.Create(incidentUpdate3); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateIncidentUp)
}
return nil
@ -168,7 +189,7 @@ func insertSampleGroups() error {
Order: 2,
}
if _, err := database.Create(group1); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateGroup)
}
group2 := &types.Group{
@ -177,7 +198,7 @@ func insertSampleGroups() error {
Order: 1,
}
if _, err := database.Create(group2); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateGroup)
}
group3 := &types.Group{
@ -186,7 +207,7 @@ func insertSampleGroups() error {
Order: 3,
}
if _, err := database.Create(group3); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateGroup)
}
return nil
}
@ -195,24 +216,24 @@ func insertSampleGroups() error {
func insertSampleCheckins() error {
s1 := SelectService(1)
checkin1 := &types.Checkin{
ServiceId: s1.Model().Id,
ServiceId: s1.Id,
Interval: 300,
GracePeriod: 300,
}
if _, err := database.Create(checkin1); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s2 := SelectService(1)
checkin2 := &types.Checkin{
ServiceId: s2.Model().Id,
ServiceId: s2.Id,
Interval: 900,
GracePeriod: 300,
}
if _, err := database.Create(checkin2); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateCheckinHit)
}
checkTime := time.Now().UTC().Add(-24 * time.Hour)
@ -224,7 +245,7 @@ func insertSampleCheckins() error {
}
if _, err := database.Create(checkHit); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateCheckinHit)
}
checkTime = checkTime.Add(10 * time.Minute)
@ -240,7 +261,7 @@ func InsertSampleHits() error {
sg.Add(1)
service := SelectService(i)
seed := time.Now().UnixNano()
log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Model().Name))
log.Infoln(fmt.Sprintf("Adding %v sample hit records to service %v", SampleHits, service.Name))
createdAt := sampleStart
p := utils.NewPerlin(2., 2., 10, seed)
go func() {
@ -249,7 +270,7 @@ func InsertSampleHits() error {
latency := p.Noise1D(hi / 500)
createdAt = createdAt.Add(60 * time.Second)
hit := &types.Hit{
Service: service.Model().Id,
Service: service.Id,
CreatedAt: createdAt,
Latency: latency,
}
@ -258,11 +279,11 @@ func InsertSampleHits() error {
}()
}
sg.Wait()
err := tx.Commit().Error()
if err != nil {
if err := tx.Commit().Error(); err != nil {
log.Errorln(err)
return types.ErrWrap(err, types.ErrorCreateSampleHits)
}
return err
return nil
}
// insertSampleCore will create a new Core for the seed
@ -276,11 +297,14 @@ func insertSampleCore() error {
Version: "test",
CreatedAt: time.Now().UTC(),
UseCdn: types.NewNullBool(false),
Footer: types.NewNullString(""),
}
_, err := database.Create(core)
if _, err := database.Create(core); err != nil {
return types.ErrWrap(err, types.ErrorCreateCore)
}
return err
return nil
}
// insertSampleUsers will create 2 admin users for a seed database
@ -293,7 +317,7 @@ func insertSampleUsers() error {
}
if _, err := database.Create(u2); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateUser)
}
u3 := &types.User{
@ -304,7 +328,7 @@ func insertSampleUsers() error {
}
if _, err := database.Create(u3); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateUser)
}
return nil
@ -320,7 +344,7 @@ func insertMessages() error {
}
if _, err := database.Create(m1); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateMessage)
}
m2 := &types.Message{
@ -332,7 +356,7 @@ func insertMessages() error {
}
if _, err := database.Create(m2); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateMessage)
}
return nil
}
@ -368,7 +392,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s6); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s7 := &types.Service{
@ -384,7 +408,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s7); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s8 := &types.Service{
@ -399,7 +423,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s8); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s9 := &types.Service{
@ -415,7 +439,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s9); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s10 := &types.Service{
@ -431,7 +455,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s10); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s11 := &types.Service{
@ -447,7 +471,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s11); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s12 := &types.Service{
@ -463,7 +487,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s12); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s13 := &types.Service{
@ -479,7 +503,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s13); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s14 := &types.Service{
@ -495,7 +519,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s14); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
s15 := &types.Service{
@ -511,7 +535,7 @@ func InsertLargeSampleData() error {
}
if _, err := database.Create(s15); err != nil {
return err
return types.ErrWrap(err, types.ErrorCreateService)
}
var dayAgo = time.Now().UTC().Add((-24 * 90) * time.Hour)
@ -527,14 +551,14 @@ func InsertLargeSampleData() error {
func insertFailureRecords(since time.Time, amount int) {
for i := int64(14); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Model().Name))
log.Infoln(fmt.Sprintf("Adding %v Failure records to service %v", amount, service.Name))
createdAt := since
for fi := 1; fi <= amount; fi++ {
createdAt = createdAt.Add(2 * time.Minute)
failure := &types.Failure{
Service: service.Model().Id,
Service: service.Id,
Issue: "testing right here",
CreatedAt: createdAt,
}
@ -545,32 +569,36 @@ func insertFailureRecords(since time.Time, amount int) {
}
// insertHitRecords will create successful Hit records for 15 services
func insertHitRecords(since time.Time, amount int) {
func insertHitRecords(since time.Time, amount int) error {
for i := int64(1); i <= 15; i++ {
service := SelectService(i)
log.Infoln(fmt.Sprintf("Adding %v hit records to service %v", amount, service.Model().Name))
log.Infoln(fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name))
createdAt := since
p := utils.NewPerlin(2, 2, 5, time.Now().UnixNano())
for hi := 1; hi <= amount; hi++ {
latency := p.Noise1D(float64(hi / 10))
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{
Service: service.Model().Id,
Service: service.Id,
CreatedAt: createdAt.UTC(),
Latency: latency,
}
database.Create(hit)
if _, err := database.Create(hit); err != nil {
return types.ErrWrap(err, types.ErrorCreateHit, service.Id)
}
}
}
return nil
}
// TmpRecords is used for testing Statping. It will create a SQLite database file
// with sample data and store it in the /tmp folder to be used by the tests.
func TmpRecords(dbFile string) error {
var sqlFile = utils.Directory + "/" + dbFile
utils.CreateDirectory(utils.Directory + "/tmp")
if err := utils.CreateDirectory(utils.Directory + "/tmp"); err != nil {
log.Error(err)
}
var tmpSqlFile = utils.Directory + "/tmp/" + types.SqliteFilename
SampleHits = 480
@ -578,19 +606,19 @@ func TmpRecords(dbFile string) error {
CoreApp = NewCore()
CoreApp.Name = "Tester"
CoreApp.Setup = true
configs := &types.DbConfig{
configs := &DbConfig{&types.DbConfig{
DbConn: "sqlite",
Project: "Tester",
Location: utils.Directory,
SqlFile: sqlFile,
}
}}
log.Infoln("saving config.yml in: " + utils.Directory)
if configs, err = CoreApp.SaveConfig(configs); err != nil {
return err
if err := configs.Save(); err != nil {
log.Error(err)
}
log.Infoln("loading config.yml from: " + utils.Directory)
if configs, err = LoadConfigFile(utils.Directory); err != nil {
return err
log.Error(err)
}
log.Infoln("connecting to database")
@ -598,37 +626,37 @@ func TmpRecords(dbFile string) error {
if exists {
log.Infoln(tmpSqlFile + " was found, copying the temp database to " + sqlFile)
if err := utils.DeleteFile(sqlFile); err != nil {
log.Infoln(sqlFile + " was not found")
log.Error(err)
}
if err := utils.CopyFile(tmpSqlFile, sqlFile); err != nil {
return err
log.Error(err)
}
log.Infoln("loading config.yml from: " + utils.Directory)
if err := CoreApp.Connect(false, utils.Directory); err != nil {
return err
log.Error(err)
}
log.Infoln("selecting the Core variable")
if _, err := SelectCore(); err != nil {
return err
log.Error(err)
}
log.Infoln("inserting notifiers into database")
if err := InsertNotifierDB(); err != nil {
return err
log.Error(err)
}
log.Infoln("inserting integrations into database")
if err := InsertIntegratorDB(); err != nil {
return err
log.Error(err)
}
log.Infoln("loading all services")
if _, err := SelectAllServices(false); err != nil {
return err
}
if err := AttachNotifiers(); err != nil {
return err
log.Error(err)
}
if err := AddIntegrations(); err != nil {
return err
log.Error(err)
}
CoreApp.Notifications = notifier.AllCommunications
return nil

View File

@ -25,20 +25,19 @@ import (
)
type Service struct {
*types.Service
*database.ServiceObj
}
type Servicer interface{}
func Services() []database.Servicer {
func Services() []*Service {
return CoreApp.services
}
// SelectService returns a *core.Service from in memory
func SelectService(id int64) database.Servicer {
func SelectService(id int64) *Service {
for _, s := range Services() {
if s.Model().Id == id {
fmt.Println("service: ", s.Model())
if s.Id == id {
s.UpdateStats()
fmt.Println("service: ", s.Name, s.Stats)
return s
}
}
@ -47,7 +46,7 @@ func SelectService(id int64) database.Servicer {
// CheckinProcess runs the checkin routine for each checkin attached to service
func CheckinProcess(s database.Servicer) {
for _, c := range s.AllCheckins() {
for _, c := range s.Checkins() {
c.Start()
go CheckinRoutine(c)
}
@ -55,31 +54,35 @@ func CheckinProcess(s database.Servicer) {
// SelectAllServices returns a slice of *core.Service to be store on []*core.Services
// should only be called once on startup.
func SelectAllServices(start bool) ([]*database.ServiceObj, error) {
func SelectAllServices(start bool) ([]*Service, error) {
srvs := database.Services()
for _, s := range srvs {
fmt.Println("services: ", s.Id, s.Name)
}
for _, s := range srvs {
if start {
service := s.Model()
service.Start()
s.Start()
CheckinProcess(s)
}
//fails := service.Service (limitedFailures)
//for _, f := range fails {
// service.Failures = append(service.Failures, f)
//}
for _, c := range s.AllCheckins() {
s.Checkins = append(s.Checkins, c)
fails := s.Failures().Last(limitedFailures)
s.Service.Failures = fails
for _, c := range s.Checkins() {
s.Service.Checkins = append(s.Service.Checkins, c.Checkin)
}
// collect initial service stats
s.Service.Stats = s.UpdateStats()
CoreApp.services = append(CoreApp.services, s)
s.UpdateStats()
CoreApp.services = append(CoreApp.services, &Service{s})
}
reorderServices()
return srvs, nil
return CoreApp.services, nil
}
func wrapFailures(f []*types.Failure) []*Failure {
var fails []*Failure
for _, v := range f {
fails = append(fails, &Failure{v})
}
return fails
}
// reorderServices will sort the services based on 'order_id'
@ -87,24 +90,10 @@ func reorderServices() {
sort.Sort(ServiceOrder(CoreApp.services))
}
// GraphData will return all hits or failures
func GraphData(q *database.GroupQuery, dbType interface{}, by database.By) []*database.TimeValue {
dbQuery, err := q.Database().GroupQuery(q, by).ToTimeValue(dbType)
if err != nil {
log.Error(err)
return nil
}
if q.FillEmpty {
return dbQuery.FillMissing(q.Start, q.End)
}
return dbQuery.ToValues()
}
// index returns a services index int for updating the []*core.Services slice
func index(s database.Servicer) int {
func index(s int64) int {
for k, service := range CoreApp.services {
if s.Model().Id == service.Model().Id {
if s == service.Id {
return k
}
}
@ -112,14 +101,13 @@ func index(s database.Servicer) int {
}
// updateService will update a service in the []*core.Services slice
func updateService(s database.Servicer) {
CoreApp.services[index(s)] = s
func updateService(s *Service) {
CoreApp.services[index(s.Id)] = s
}
// Delete will remove a service from the database, it will also end the service checking go routine
func Delete(srv database.Servicer) error {
i := index(srv)
s := srv.Model()
func (s *Service) Delete() error {
i := index(s.Id)
err := database.Delete(s)
if err != nil {
log.Errorln(fmt.Sprintf("Failed to delete service %v. %v", s.Name, err))
@ -129,13 +117,12 @@ func Delete(srv database.Servicer) error {
slice := CoreApp.services
CoreApp.services = append(slice[:i], slice[i+1:]...)
reorderServices()
notifier.OnDeletedService(s)
notifier.OnDeletedService(s.Service)
return err
}
// Update will update a service in the database, the service's checking routine can be restarted by passing true
func Update(srv database.Servicer, restart bool) error {
s := srv.Model()
func Update(s *Service, restart bool) error {
err := database.Update(s)
if err != nil {
log.Errorln(fmt.Sprintf("Failed to update service %v. %v", s.Name, err))
@ -151,12 +138,12 @@ func Update(srv database.Servicer, restart bool) error {
if restart {
s.Close()
s.Start()
s.SleepDuration = time.Duration(s.Interval) * time.Second
go ServiceCheckQueue(srv, true)
s.SleepDuration = s.Duration()
go ServiceCheckQueue(s, true)
}
reorderServices()
updateService(srv)
notifier.OnUpdatedService(s)
updateService(s)
notifier.OnUpdatedService(s.Service)
return err
}
@ -169,10 +156,11 @@ func Create(srv database.Servicer, check bool) (int64, error) {
log.Errorln(fmt.Sprintf("Failed to create service %v #%v: %v", s.Name, s.Id, err))
return 0, err
}
service := &Service{s}
s.Start()
go ServiceCheckQueue(srv, check)
CoreApp.services = append(CoreApp.services, srv)
CoreApp.services = append(CoreApp.services, service)
go ServiceCheckQueue(service, check)
reorderServices()
notifier.OnNewService(s)
notifier.OnNewService(s.Service)
return s.Id, nil
}

View File

@ -16,9 +16,10 @@
package core
import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
@ -29,13 +30,12 @@ var (
func TestCreateCheckin(t *testing.T) {
service := SelectService(1)
testCheckin = ReturnCheckin(&types.Checkin{
checkin := &types.Checkin{
ServiceId: service.Id,
Interval: 10,
GracePeriod: 5,
ApiKey: utils.RandomString(7),
})
id, err := testCheckin.Create()
}
id, err := database.Create(checkin)
assert.Nil(t, err)
assert.NotZero(t, id)
assert.NotEmpty(t, testCheckin.ApiKey)
@ -46,13 +46,13 @@ func TestCreateCheckin(t *testing.T) {
func TestSelectCheckin(t *testing.T) {
service := SelectService(1)
checkins := service.AllCheckins()
checkins := service.Checkins()
assert.NotNil(t, checkins)
assert.Equal(t, 1, len(checkins))
testCheckin = checkins[0]
assert.Equal(t, int64(10), testCheckin.Interval)
assert.Equal(t, int64(5), testCheckin.GracePeriod)
assert.Equal(t, 7, len(testCheckin.ApiKey))
c := checkins[0]
assert.Equal(t, int64(10), c.Interval)
assert.Equal(t, int64(5), c.GracePeriod)
assert.Equal(t, 7, len(c.ApiKey))
}
func TestUpdateCheckin(t *testing.T) {
@ -63,7 +63,7 @@ func TestUpdateCheckin(t *testing.T) {
assert.NotZero(t, id)
assert.NotEmpty(t, testCheckin.ApiKey)
service := SelectService(1)
checkin := service.AllCheckins()[0]
checkin := service.Checkins()[0]
assert.Equal(t, int64(60), checkin.Interval)
assert.Equal(t, int64(15), checkin.GracePeriod)
t.Log(testCheckin.Expected())
@ -72,15 +72,16 @@ func TestUpdateCheckin(t *testing.T) {
func TestCreateCheckinHits(t *testing.T) {
service := SelectService(1)
checkins := service.AllCheckins()
checkins := service.Checkins()
assert.Equal(t, 1, len(checkins))
created := time.Now().UTC().Add(-60 * time.Second)
hit := ReturnCheckinHit(&types.CheckinHit{
hit := &types.CheckinHit{
Checkin: testCheckin.Id,
From: "192.168.1.1",
CreatedAt: created,
})
hit.Create()
}
_, err := database.Create(hit)
require.Nil(t, err)
hits := testCheckin.AllHits()
assert.Equal(t, 1, len(hits))
}
@ -88,13 +89,13 @@ func TestCreateCheckinHits(t *testing.T) {
func TestSelectCheckinMethods(t *testing.T) {
time.Sleep(5 * time.Second)
service := SelectService(1)
checkins := service.AllCheckins()
checkins := service.Checkins()
assert.NotNil(t, checkins)
lastHit := testCheckin.Last()
assert.Equal(t, float64(60), testCheckin.Period().Seconds())
assert.Equal(t, float64(15), testCheckin.Grace().Seconds())
t.Log(testCheckin.Expected())
lastHit := checkins[0]
assert.True(t, testCheckin.Expected().Seconds() < -5)
assert.False(t, lastHit.CreatedAt.IsZero())
assert.Equal(t, "A minute ago", lastHit.Ago())
}

View File

@ -16,9 +16,11 @@
package core
import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"testing"
"time"
)
@ -28,7 +30,7 @@ var (
)
func TestSelectHTTPService(t *testing.T) {
services, err := CoreApp.SelectAllServices(false)
services, err := SelectAllServices(false)
assert.Nil(t, err)
assert.Equal(t, 15, len(services))
assert.Equal(t, "Google", services[0].Name)
@ -36,12 +38,11 @@ func TestSelectHTTPService(t *testing.T) {
}
func TestSelectAllServices(t *testing.T) {
services := CoreApp.Services
services := CoreApp.services
for _, s := range services {
service := s.(*Service)
service.Check(false)
assert.False(t, service.IsRunning())
t.Logf("ID: %v %v\n", service.Id, service.Name)
CheckService(s, false)
assert.False(t, s.IsRunning())
t.Logf("ID: %v %v\n", s.Id, s.Name)
}
assert.Equal(t, 15, len(services))
}
@ -54,7 +55,7 @@ func TestServiceDowntime(t *testing.T) {
}
func TestSelectTCPService(t *testing.T) {
services := CoreApp.Services
services := CoreApp.services
assert.Equal(t, 15, len(services))
service := SelectService(5)
assert.NotNil(t, service)
@ -67,8 +68,10 @@ func TestUpdateService(t *testing.T) {
assert.Equal(t, "Google", service.Name)
service.Name = "Updated Google"
service.Interval = 5
err := service.Update(true)
assert.Nil(t, err)
err := database.Update(service)
require.Nil(t, err)
// check if updating pointer array shutdown any other service
service = SelectService(1)
assert.Equal(t, "Updated Google", service.Name)
@ -76,19 +79,20 @@ func TestUpdateService(t *testing.T) {
}
func TestUpdateAllServices(t *testing.T) {
services, err := CoreApp.SelectAllServices(false)
assert.Nil(t, err)
services, err := SelectAllServices(false)
require.Nil(t, err)
for k, srv := range services {
srv.Name = "Changed " + srv.Name
srv.Interval = k + 3
err := srv.Update(true)
assert.Nil(t, err)
err := database.Update(srv)
require.Nil(t, err)
}
}
func TestServiceHTTPCheck(t *testing.T) {
service := SelectService(1)
service.Check(true)
CheckService(service, true)
assert.Equal(t, "Changed Updated Google", service.Name)
assert.True(t, service.Online)
}
@ -104,7 +108,7 @@ func TestCheckHTTPService(t *testing.T) {
func TestServiceTCPCheck(t *testing.T) {
service := SelectService(5)
service.Check(true)
CheckService(service, true)
assert.Equal(t, "Changed Google DNS", service.Name)
assert.True(t, service.Online)
}
@ -130,23 +134,17 @@ func TestServiceOnline24Hours(t *testing.T) {
func TestServiceAvgUptime(t *testing.T) {
since := utils.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)
assert.NotEqual(t, "0.00", service.AvgUptime(since))
assert.NotEqual(t, "0.00", service.AvgTime())
service2 := SelectService(5)
assert.Equal(t, "100", service2.AvgUptime(since))
assert.Equal(t, "100", service2.AvgTime())
service3 := SelectService(13)
assert.NotEqual(t, "0", service3.AvgUptime(since))
service4 := SelectService(15)
assert.NotEqual(t, "0", service4.AvgUptime(since))
}
func TestServiceSum(t *testing.T) {
service := SelectService(5)
sum := service.Sum()
assert.NotZero(t, sum)
}
func TestCreateService(t *testing.T) {
s := ReturnService(&types.Service{
s := &types.Service{
Name: "That'll do 🐢",
Domain: "https://www.youtube.com/watch?v=rjQtzV9IZ0Q",
ExpectedStatus: 200,
@ -155,12 +153,11 @@ func TestCreateService(t *testing.T) {
Method: "GET",
Timeout: 20,
GroupId: 1,
})
var err error
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
}
obj, err := database.Create(s)
require.Nil(t, err)
assert.NotZero(t, obj.Id)
newService := SelectService(obj.Id)
assert.Equal(t, "That'll do 🐢", newService.Name)
}
@ -170,7 +167,7 @@ func TestViewNewService(t *testing.T) {
}
func TestCreateFailingHTTPService(t *testing.T) {
s := ReturnService(&types.Service{
s := &types.Service{
Name: "Bad URL",
Domain: "http://localhost/iamnothere",
ExpectedStatus: 200,
@ -179,12 +176,11 @@ func TestCreateFailingHTTPService(t *testing.T) {
Method: "GET",
Timeout: 5,
GroupId: 1,
})
var err error
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
}
obj, err := database.Create(s)
require.Nil(t, err)
assert.NotZero(t, obj.Id)
newService := SelectService(obj.Id)
assert.Equal(t, "Bad URL", newService.Name)
t.Log("new service ID: ", newServiceId)
}
@ -192,13 +188,13 @@ func TestCreateFailingHTTPService(t *testing.T) {
func TestServiceFailedCheck(t *testing.T) {
service := SelectService(17)
assert.Equal(t, "Bad URL", service.Name)
service.Check(false)
CheckService(service, false)
assert.Equal(t, "Bad URL", service.Name)
assert.False(t, service.Online)
}
func TestCreateFailingTCPService(t *testing.T) {
s := ReturnService(&types.Service{
s := &types.Service{
Name: "Bad TCP",
Domain: "localhost",
Port: 5050,
@ -206,50 +202,51 @@ func TestCreateFailingTCPService(t *testing.T) {
Type: "tcp",
Timeout: 5,
GroupId: 1,
})
}
var err error
newServiceId, err = s.Create(false)
obj, err := database.Create(s)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
assert.NotZero(t, obj.Id)
newService := SelectService(obj.Id)
assert.Equal(t, "Bad TCP", newService.Name)
t.Log("new failing tcp service ID: ", newServiceId)
}
func TestServiceFailedTCPCheck(t *testing.T) {
service := SelectService(newServiceId)
service.Check(false)
CheckService(service, false)
assert.Equal(t, "Bad TCP", service.Name)
assert.False(t, service.Online)
}
func TestCreateServiceFailure(t *testing.T) {
service := SelectService(8)
fail := &types.Failure{
Issue: "This is not an issue, but it would container HTTP response errors.",
Method: "http",
Service: service.Id,
}
service := SelectService(8)
id, err := service.CreateFailure(fail)
obj, err := database.Create(fail)
assert.Nil(t, err)
assert.NotZero(t, id)
assert.NotZero(t, obj.Id)
}
func TestDeleteService(t *testing.T) {
service := SelectService(newServiceId)
count, err := CoreApp.SelectAllServices(false)
count, err := SelectAllServices(false)
assert.Nil(t, err)
assert.Equal(t, 18, len(count))
err = service.Delete()
assert.Nil(t, err)
services := CoreApp.Services
services := CoreApp.services
assert.Equal(t, 17, len(services))
}
func TestServiceCloseRoutine(t *testing.T) {
s := ReturnService(new(types.Service))
s := new(Service)
s.Name = "example"
s.Domain = "https://google.com"
s.Type = "http"
@ -260,7 +257,7 @@ func TestServiceCloseRoutine(t *testing.T) {
assert.True(t, s.IsRunning())
t.Log(s.Checkpoint)
t.Log(s.SleepDuration)
go s.CheckQueue(false)
go ServiceCheckQueue(s, false)
t.Log(s.Checkpoint)
t.Log(s.SleepDuration)
time.Sleep(5 * time.Second)
@ -274,7 +271,7 @@ func TestServiceCloseRoutine(t *testing.T) {
}
func TestServiceCheckQueue(t *testing.T) {
s := ReturnService(new(types.Service))
s := new(Service)
s.Name = "example"
s.Domain = "https://google.com"
s.Type = "http"
@ -283,7 +280,7 @@ func TestServiceCheckQueue(t *testing.T) {
s.Interval = 1
s.Start()
assert.True(t, s.IsRunning())
go s.CheckQueue(false)
go ServiceCheckQueue(s, false)
go func() {
time.Sleep(5 * time.Second)
@ -300,14 +297,14 @@ func TestServiceCheckQueue(t *testing.T) {
}
func TestDNScheckService(t *testing.T) {
s := ReturnService(new(types.Service))
s := new(Service)
s.Name = "example"
s.Domain = "http://localhost:9000"
s.Type = "http"
s.Method = "GET"
s.ExpectedStatus = 200
s.Interval = 1
amount, err := s.dnsCheck()
amount, err := dnsCheck(s)
assert.Nil(t, err)
assert.NotZero(t, amount)
}
@ -317,25 +314,13 @@ func TestSelectServiceLink(t *testing.T) {
assert.Equal(t, "google", service.Permalink.String)
}
func TestDbtimestamp(t *testing.T) {
CoreApp.Config.DbConn = "mysql"
query := Dbtimestamp("minute", "latency")
assert.Equal(t, "CONCAT(date_format(created_at, '%Y-%m-%d %H:00:00')) AS timeframe, AVG(latency) AS value", query)
CoreApp.Config.DbConn = "postgres"
query = Dbtimestamp("minute", "latency")
assert.Equal(t, "date_trunc('minute', created_at) AS timeframe, AVG(latency) AS value", query)
CoreApp.Config.DbConn = "sqlite"
query = Dbtimestamp("minute", "latency")
assert.Equal(t, "datetime((strftime('%s', created_at) / 60) * 60, 'unixepoch') AS timeframe, AVG(latency) as value", query)
}
func TestGroup_Create(t *testing.T) {
group := &Group{&types.Group{
group := &types.Group{
Name: "Testing",
}}
newGroupId, err := group.Create()
}
obj, err := database.Create(group)
assert.Nil(t, err)
assert.NotZero(t, newGroupId)
assert.NotZero(t, obj.Id)
}
func TestGroup_Services(t *testing.T) {

View File

@ -96,16 +96,21 @@ func SelectAllUsers() []*types.User {
// AuthUser will return the User and a boolean if authentication was correct.
// AuthUser accepts username, and password as a string
func AuthUser(username, password string) (*User, bool) {
user, err := SelectUsername(username)
func AuthUser(username, password string) (*types.User, bool) {
user, err := database.UserByUsername(username)
if err != nil {
log.Warnln(fmt.Errorf("user %v not found", username))
return nil, false
}
fmt.Println(username, password)
fmt.Println(username, user.Password)
if CheckHash(password, user.Password) {
user.UpdatedAt = time.Now().UTC()
user.Update()
return user, true
database.Update(user)
return user.User, true
}
return nil, false
}

View File

@ -16,26 +16,26 @@
package core
import (
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/stretchr/testify/assert"
"testing"
)
func TestCreateUser(t *testing.T) {
user := ReturnUser(&types.User{
user := &types.User{
Username: "hunter",
Password: "password123",
Email: "test@email.com",
Admin: types.NewNullBool(true),
})
userId, err := user.Create()
}
obj, err := database.Create(user)
assert.Nil(t, err)
assert.NotZero(t, userId)
assert.NotZero(t, obj.Id)
}
func TestSelectAllUsers(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
users := SelectAllUsers()
assert.Equal(t, 3, len(users))
}
@ -66,20 +66,19 @@ func TestUpdateUser(t *testing.T) {
}
func TestCreateUser2(t *testing.T) {
user := ReturnUser(&types.User{
user := &types.User{
Username: "hunterlong",
Password: "password123",
Email: "User@email.com",
Admin: types.NewNullBool(true),
})
userId, err := user.Create()
}
obj, err := database.Create(user)
assert.Nil(t, err)
assert.NotZero(t, userId)
assert.NotZero(t, obj.Id)
}
func TestSelectAllUsersAgain(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
users := SelectAllUsers()
assert.Equal(t, 4, len(users))
}

View File

@ -3,6 +3,7 @@ package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"time"
)
@ -20,6 +21,21 @@ type Checkiner interface {
Object() *CheckinObj
}
func (c *CheckinObj) BeforeCreate() (err error) {
c.ApiKey = utils.RandomString(7)
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
c.UpdatedAt = time.Now().UTC()
}
return
}
func (c *CheckinObj) BeforeDelete(tx Database) (err error) {
q := tx.Services().Where("id = ?", c.ServiceId).
Update("group_id", 0)
return q.Error()
}
func Checkin(id int64) (*CheckinObj, error) {
var checkin types.Checkin
query := database.Checkins().Where("id = ?", id)

View File

@ -1,11 +1,14 @@
package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"reflect"
)
type CrudObject interface {
Create()
}
type Object struct {
Id int64
model interface{}
@ -25,41 +28,35 @@ func wrapObject(id int64, model interface{}, db Database) *Object {
}
func modelId(model interface{}) int64 {
fmt.Printf("%T\n", model)
switch model.(type) {
case *types.Core:
return 0
default:
iface := reflect.ValueOf(model)
field := iface.Elem().FieldByName("Id")
return field.Int()
}
func toModel(model interface{}) Database {
switch model.(type) {
case *types.Core:
return database.Model(&types.Core{}).Table("core")
default:
return database.Model(&model)
}
}
func Create(data interface{}) (*Object, error) {
model := toModel(data)
query := model.Create(data)
if query.Error() != nil {
return nil, query.Error()
model := database.Model(&data)
if err := model.Create(data).Error(); err != nil {
return nil, err
}
obj := &Object{
Id: modelId(data),
model: data,
db: model,
}
return obj, query.Error()
return obj, nil
}
func Update(data interface{}) error {
model := toModel(data)
model := database.Model(&data)
return model.Update(&data).Error()
}
func Delete(data interface{}) error {
model := toModel(data)
model := database.Model(&data)
return model.Delete(data).Error()
}

View File

@ -113,8 +113,6 @@ type Database interface {
Requests(*http.Request, isObject) Database
GroupQuery(query *GroupQuery, by By) GroupByer
Objects
}
@ -193,6 +191,9 @@ type Db struct {
// Openw is a drop-in replacement for Open()
func Openw(dialect string, args ...interface{}) (db Database, err error) {
gorm.NowFunc = func() time.Time {
return time.Now().UTC()
}
gormdb, err := gorm.Open(dialect, args...)
if err != nil {
return nil, err

View File

@ -34,10 +34,10 @@ func (f *FailureObj) DeleteAll() error {
return query.Error()
}
func (f *FailureObj) Last(amount int) *types.Failure {
var fail types.Failure
f.o.db.Limit(amount).Last(&fail)
return &fail
func (f *FailureObj) Last(amount int) []*types.Failure {
var fail []*types.Failure
f.o.db.Limit(amount).Find(&fail)
return fail
}
func (f *FailureObj) Count() int {

View File

@ -15,7 +15,7 @@ type GroupBy struct {
}
type GroupByer interface {
ToTimeValue(interface{}) (*TimeVar, error)
ToTimeValue() (*TimeVar, error)
}
type By string
@ -25,7 +25,6 @@ func (b By) String() string {
}
type GroupQuery struct {
db Database
Start time.Time
End time.Time
Group string
@ -33,6 +32,8 @@ type GroupQuery struct {
Limit int
Offset int
FillEmpty bool
db Database
}
func (b GroupQuery) Find(data interface{}) error {
@ -50,25 +51,16 @@ var (
}
)
func (db *Db) GroupQuery(q *GroupQuery, by By) GroupByer {
dbQuery := db.MultipleSelects(
db.SelectByTime(q.Group),
by.String(),
).Group("timeframe")
return &GroupBy{dbQuery, q}
}
type TimeVar struct {
g *GroupBy
g *GroupQuery
data []*TimeValue
}
func (t *TimeVar) ToValues() []*TimeValue {
return t.data
func (t *TimeVar) ToValues() ([]*TimeValue, error) {
return t.data, nil
}
func (g *GroupBy) toFloatRows() []*TimeValue {
func (g *GroupQuery) toFloatRows() []*TimeValue {
rows, err := g.db.Rows()
if err != nil {
return nil
@ -77,7 +69,12 @@ func (g *GroupBy) toFloatRows() []*TimeValue {
for rows.Next() {
var timeframe time.Time
amount := float64(0)
rows.Scan(&timeframe, &amount)
if err := rows.Scan(&timeframe, &amount); err != nil {
log.Errorln(err)
}
fmt.Println("float rows: ", timeframe, amount)
newTs := types.FixedTime(timeframe, g.duration())
data = append(data, &TimeValue{
Timeframe: newTs,
@ -87,17 +84,41 @@ func (g *GroupBy) toFloatRows() []*TimeValue {
return data
}
func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) {
// GraphData will return all hits or failures
func (g *GroupQuery) GraphData(by By) ([]*TimeValue, error) {
dbQuery := g.db.MultipleSelects(
g.db.SelectByTime(g.Group),
by.String(),
).Group("timeframe")
g.db = dbQuery
caller, err := g.ToTimeValue()
if err != nil {
return nil, err
}
if g.FillEmpty {
return caller.FillMissing(g.Start, g.End)
}
return caller.ToValues()
}
func (g *GroupQuery) ToTimeValue() (*TimeVar, error) {
rows, err := g.db.Rows()
if err != nil {
return nil, err
}
var data []*TimeValue
for rows.Next() {
var timeframe time.Time
var timeframe string
amount := float64(0)
rows.Scan(&timeframe, &amount)
newTs := types.FixedTime(timeframe, g.duration())
if err := rows.Scan(&timeframe, &amount); err != nil {
log.Error(err, timeframe)
}
trueTime, _ := g.db.ParseTime(timeframe)
newTs := types.FixedTime(trueTime, g.duration())
data = append(data, &TimeValue{
Timeframe: newTs,
Amount: amount,
@ -106,7 +127,7 @@ func (g *GroupBy) ToTimeValue(dbType interface{}) (*TimeVar, error) {
return &TimeVar{g, data}, nil
}
func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue {
func (t *TimeVar) FillMissing(current, end time.Time) ([]*TimeValue, error) {
timeMap := make(map[string]float64)
var validSet []*TimeValue
dur := t.g.duration()
@ -132,11 +153,11 @@ func (t *TimeVar) FillMissing(current, end time.Time) []*TimeValue {
currentStr = types.FixedTime(current, t.g.duration())
}
return validSet
return validSet, nil
}
func (g *GroupBy) duration() time.Duration {
switch g.query.Group {
func (g *GroupQuery) duration() time.Duration {
switch g.Group {
case "second":
return types.Second
case "minute":
@ -175,6 +196,8 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
limit = 10000
}
db := o.object().db
query := &GroupQuery{
Start: time.Unix(startField, 0).UTC(),
End: time.Unix(endField, 0).UTC(),
@ -183,10 +206,9 @@ func ParseQueries(r *http.Request, o isObject) *GroupQuery {
Limit: int(limit),
Offset: int(offset),
FillEmpty: fill,
db: db,
}
db := o.object().db
if query.Limit != 0 {
db = db.Limit(query.Limit)
}

View File

@ -15,10 +15,25 @@ func (h *HitObj) All() []*types.Hit {
return fails
}
func (h *HitObj) Last(amount int) *types.Hit {
var hits types.Hit
func (s *ServiceObj) CreateHit(hit *types.Hit) *HitObj {
hit.Service = s.Id
database.Create(hit)
return &HitObj{wrapObject(hit.Id, hit, database.Hits().Where("id = ?", hit.Id))}
}
func (h *HitObj) Sum() float64 {
result := struct {
amount float64
}{0}
h.o.db.Select("AVG(latency) as amount").Scan(&result).Debug()
return result.amount
}
func (h *HitObj) Last(amount int) []*types.Hit {
var hits []*types.Hit
h.o.db.Limit(amount).Find(&hits)
return &hits
return hits
}
func (h *HitObj) Since(t time.Time) []*types.Hit {

60
database/routines.go Normal file
View File

@ -0,0 +1,60 @@
package database
import (
"fmt"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"os"
"time"
)
var (
log = utils.Log
removeRowsAfter = types.Month * 6
maintenceDuration = types.Hour
)
func StartMaintenceRoutine() {
dur := os.Getenv("REMOVE_AFTER")
var removeDur time.Duration
if dur != "" {
parsedDur, err := time.ParseDuration(dur)
if err != nil {
log.Errorf("could not parse duration: %s, using default: %s", dur, removeRowsAfter.String())
removeDur = removeRowsAfter
} else {
removeDur = parsedDur
}
} else {
removeDur = removeRowsAfter
}
log.Infof("Service Failure and Hit records will be automatically removed after %s", removeDur.String())
go databaseMaintence(removeDur)
}
// databaseMaintence will automatically delete old records from 'failures' and 'hits'
// this function is currently set to delete records 7+ days old every 60 minutes
func databaseMaintence(dur time.Duration) {
deleteAfter := time.Now().UTC().Add(dur)
for range time.Tick(maintenceDuration) {
log.Infof("Deleting failures older than %s", dur.String())
DeleteAllSince("failures", deleteAfter)
log.Infof("Deleting hits older than %s", dur.String())
DeleteAllSince("hits", deleteAfter)
maintenceDuration = types.Hour
}
}
// DeleteAllSince will delete a specific table's records based on a time.
func DeleteAllSince(table string, date time.Time) {
sql := fmt.Sprintf("DELETE FROM %v WHERE created_at < '%v';", table, database.FormatTime(date))
db := database.Exec(sql)
if db.Error() != nil {
log.Warnln(db.Error())
}
}

View File

@ -16,18 +16,22 @@ type ServiceObj struct {
}
type Servicer interface {
Hits() *HitObj
Failures() *FailureObj
AllCheckins() []*CheckinObj
Model() *types.Service
Interval() time.Duration
Checkins() []*CheckinObj
DowntimeText() string
UpdateStats()
Model() *ServiceObj
Hittable
}
type Hittable interface {
CreateHit(*types.Hit) (int64, error)
Hits() *HitObj
CreateHit(hit *types.Hit) *HitObj
}
func (s *ServiceObj) Model() *ServiceObj {
return s
}
func Service(id int64) (*ServiceObj, error) {
@ -52,7 +56,7 @@ func Services() []*ServiceObj {
return wrapServices(services, db)
}
func (s *ServiceObj) AllCheckins() []*CheckinObj {
func (s *ServiceObj) Checkins() []*CheckinObj {
var checkins []*types.Checkin
query := database.Checkins().Where("service = ?", s.Id)
query.Find(&checkins)
@ -61,7 +65,10 @@ func (s *ServiceObj) AllCheckins() []*CheckinObj {
func (s *ServiceObj) DowntimeText() string {
last := s.Failures().Last(1)
return parseError(last)
if len(last) == 0 {
return ""
}
return parseError(last[0])
}
// ParseError returns a human readable error for a Failure
@ -116,16 +123,7 @@ func parseError(f *types.Failure) string {
return f.Issue
}
func (s *ServiceObj) Interval() time.Duration {
return time.Duration(s.Service.Interval) * time.Second
}
func (s *ServiceObj) Model() *types.Service {
return s.Service
}
func (s *ServiceObj) Hits() *HitObj {
fmt.Println("hits")
query := database.Hits().Where("service = ?", s.Id)
return &HitObj{wrapObject(s.Id, nil, query)}
}
@ -149,34 +147,27 @@ func (s *ServiceObj) object() *Object {
return s.o
}
func (s *ServiceObj) UpdateStats() *types.Stats {
func (s *ServiceObj) UpdateStats() {
s.Online24Hours = s.OnlineDaysPercent(1)
s.Online7Days = s.OnlineDaysPercent(7)
s.AvgResponse = s.AvgTime()
s.FailuresLast24Hours = len(s.Failures().Since(time.Now().Add(-time.Hour * 24)))
return s.Stats
s.FailuresLast24Hours = len(s.Failures().Since(time.Now().UTC().Add(-time.Hour * 24)))
s.Stats = &types.Stats{
Failures: s.Failures().Count(),
Hits: s.Hits().Count(),
}
}
// AvgTime will return the average amount of time for a service to response back successfully
func (s *ServiceObj) AvgTime() float64 {
var sum []float64
database.Hits().
Select("AVG(latency) as amount").
Where("service = ?", s.Id).Pluck("amount", &sum).Debug()
sum := s.Hits().Sum()
return sum
}
sumTotal := float64(0)
for _, v := range sum {
sumTotal += v
}
total := s.Hits().Count()
if total == 0 {
return 0
}
avg := sumTotal / float64(total) * 100
f, _ := strconv.ParseFloat(fmt.Sprintf("%0.0f", avg*10), 32)
return f
// AvgUptime will return the average amount of time for a service to response back successfully
func (s *ServiceObj) AvgUptime(since time.Time) float64 {
sum := s.Hits().Sum()
return sum
}
// OnlineDaysPercent returns the service's uptime percent within last 24 hours
@ -211,12 +202,12 @@ func (s *ServiceObj) OnlineSince(ago time.Time) float32 {
func (s *ServiceObj) Downtime() time.Duration {
hits := s.Hits().Last(1)
fail := s.Failures().Last(1)
if fail == nil {
if len(fail) == 0 {
return time.Duration(0)
}
if hits == nil {
return time.Now().UTC().Sub(fail.CreatedAt.UTC())
if len(fail) == 0 {
return time.Now().UTC().Sub(fail[0].CreatedAt.UTC())
}
since := fail.CreatedAt.UTC().Sub(hits.CreatedAt.UTC())
since := fail[0].CreatedAt.UTC().Sub(hits[0].CreatedAt.UTC())
return since
}

2
go.mod
View File

@ -18,8 +18,6 @@ require (
github.com/go-mail/mail v2.3.1+incompatible
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/gorilla/mux v1.7.3
github.com/gorilla/websocket v1.4.1 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/jinzhu/gorm v1.9.11
github.com/joho/godotenv v1.3.0
github.com/lib/pq v1.2.0 // indirect

4
go.sum
View File

@ -86,11 +86,7 @@ github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2z
github.com/gorilla/mux v1.7.3 h1:gnP5JzjVOuiZD07fKKToCAOjS0yOpj/qPETTXCCS6hw=
github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA=
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=

View File

@ -33,6 +33,7 @@ func init() {
func TestResetDatabase(t *testing.T) {
err := core.TmpRecords("handlers.db")
t.Log(err)
require.Nil(t, err)
require.NotNil(t, core.CoreApp)
require.NotNil(t, core.CoreApp.Config)

View File

@ -95,10 +95,13 @@ func checkinDeleteHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(fmt.Errorf("checkin %v was not found", vars["api"]), w, r)
return
}
err := checkin.Delete()
if err != nil {
if err := database.Delete(checkin); err != nil {
sendErrorJson(err, w, r)
return
}
checkin.Delete()
sendJsonAction(checkin, "delete", w, r)
}

View File

@ -21,6 +21,7 @@ import (
"github.com/dgrijalva/jwt-go"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
"os"
@ -183,7 +184,7 @@ func removeJwtToken(w http.ResponseWriter) {
})
}
func setJwtToken(user *core.User, w http.ResponseWriter) (JwtClaim, string) {
func setJwtToken(user *types.User, w http.ResponseWriter) (JwtClaim, string) {
expirationTime := time.Now().Add(72 * time.Hour)
jwtClaim := JwtClaim{
Username: user.Username,

View File

@ -21,6 +21,7 @@ import (
"github.com/gorilla/mux"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"net/http"
)
@ -32,11 +33,19 @@ func apiAllGroupHandler(r *http.Request) interface{} {
return groups
}
func flattenGroups(groups []*core.Group) []*types.Group {
var groupers []*types.Group
for _, g := range groups {
groupers = append(groupers, g.Group)
}
return groupers
}
// apiGroupHandler will show a single group
func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
if group.Id == 0 {
sendErrorJson(errors.New("group not found"), w, r)
return
}
@ -47,7 +56,7 @@ func apiGroupHandler(w http.ResponseWriter, r *http.Request) {
func apiGroupUpdateHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
if group.Id == 0 {
sendErrorJson(errors.New("group not found"), w, r)
return
}
@ -82,7 +91,7 @@ func apiCreateGroupHandler(w http.ResponseWriter, r *http.Request) {
func apiGroupDeleteHandler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
group := core.SelectGroup(utils.ToInt(vars["id"]))
if group == nil {
if group.Id == 0 {
sendErrorJson(errors.New("group not found"), w, r)
return
}

View File

@ -45,7 +45,7 @@ func prometheusHandler(w http.ResponseWriter, r *http.Request) {
if !v.Online {
online = 0
}
met := fmt.Sprintf("statping_service_failures{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, len(v.Failures))
met := fmt.Sprintf("statping_service_failures{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, v.Failures().Count())
met += fmt.Sprintf("statping_service_latency{id=\"%v\" name=\"%v\"} %0.0f\n", v.Id, v.Name, (v.Latency * 100))
met += fmt.Sprintf("statping_service_online{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, online)
met += fmt.Sprintf("statping_service_status_code{id=\"%v\" name=\"%v\"} %v\n", v.Id, v.Name, v.LastStatusCode)

View File

@ -22,7 +22,6 @@ import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/utils"
"net/http"
"os"
)
var (
@ -41,15 +40,18 @@ func Router() *mux.Router {
CacheStorage = NewStorage()
r := mux.NewRouter().StrictSlash(true)
if os.Getenv("AUTH_USERNAME") != "" && os.Getenv("AUTH_PASSWORD") != "" {
authUser = os.Getenv("AUTH_USERNAME")
authPass = os.Getenv("AUTH_PASSWORD")
authUser := utils.Getenv("AUTH_USERNAME", "").(string)
authPass := utils.Getenv("AUTH_PASSWORD", "").(string)
if authUser != "" && authPass != "" {
r.Use(basicAuthHandler)
}
if os.Getenv("BASE_PATH") != "" {
basePath = "/" + os.Getenv("BASE_PATH") + "/"
r = r.PathPrefix("/" + os.Getenv("BASE_PATH")).Subrouter()
bPath := utils.Getenv("BASE_PATH", "").(string)
if bPath != "" {
basePath = "/" + bPath + "/"
r = r.PathPrefix("/" + bPath).Subrouter()
r.Handle("", http.HandlerFunc(indexHandler))
} else {
r.Handle("/", http.HandlerFunc(indexHandler))

View File

@ -30,35 +30,37 @@ type scope struct {
func (s scope) MarshalJSON() ([]byte, error) {
svc := reflect.ValueOf(s.data)
if svc.Kind() == reflect.Slice {
alldata := make([]map[string]interface{}, 0)
alldata := make([]map[string]interface{}, svc.Len())
for i := 0; i < svc.Len(); i++ {
objIndex := svc.Index(i)
if objIndex.Kind() == reflect.Ptr {
objIndex = objIndex.Elem()
}
alldata = append(alldata, SafeJson(objIndex.Interface(), s.scope))
alldata[i] = SafeJson(objIndex, s.scope)
}
return json.Marshal(alldata)
}
return json.Marshal(SafeJson(svc.Interface(), s.scope))
return json.Marshal(SafeJson(svc, s.scope))
}
func SafeJson(input interface{}, scope string) map[string]interface{} {
func SafeJson(val reflect.Value, scope string) map[string]interface{} {
thisData := make(map[string]interface{})
t := reflect.TypeOf(input)
elem := reflect.ValueOf(input)
d, _ := json.Marshal(input)
if val.Kind() == reflect.Interface && !val.IsNil() {
elm := val.Elem()
if elm.Kind() == reflect.Ptr && !elm.IsNil() && elm.Elem().Kind() == reflect.Ptr {
val = elm
}
}
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
var raw map[string]*json.RawMessage
json.Unmarshal(d, &raw)
for i := 0; i < val.NumField(); i++ {
valueField := val.Field(i)
typeField := val.Type().Field(i)
tagVal := typeField.Tag
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
tag := field.Tag.Get("scope")
tag := tagVal.Get("scope")
tags := strings.Split(tag, ",")
jTags := field.Tag.Get("json")
jTags := tagVal.Get("json")
jsonTag := strings.Split(jTags, ",")
if len(jsonTag) == 0 {
@ -69,21 +71,19 @@ func SafeJson(input interface{}, scope string) map[string]interface{} {
continue
}
trueValue := elem.Field(i).Interface()
if len(jsonTag) == 2 {
if jsonTag[1] == "omitempty" && trueValue == "" {
if jsonTag[1] == "omitempty" && valueField.Interface() == "" {
continue
}
}
if tag == "" {
thisData[jsonTag[0]] = trueValue
thisData[jsonTag[0]] = valueField.Interface()
continue
}
if forTag(tags, scope) {
thisData[jsonTag[0]] = trueValue
thisData[jsonTag[0]] = valueField.Interface()
}
}
return thisData

View File

@ -46,11 +46,11 @@ func reorderServiceHandler(w http.ResponseWriter, r *http.Request) {
func apiServiceHandler(r *http.Request) interface{} {
vars := mux.Vars(r)
servicer := core.SelectService(utils.ToInt(vars["id"])).Model()
servicer := core.SelectService(utils.ToInt(vars["id"]))
if servicer == nil {
return errors.New("service not found")
}
return *servicer
return servicer.Service
}
func apiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
@ -130,8 +130,12 @@ func apiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("latency"))
returnJson(obj, w, r)
objs, err := groupQuery.GraphData(database.ByAverage("latency"))
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(objs, w, r)
}
func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
@ -141,10 +145,16 @@ func apiServiceFailureDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Failure{}, database.ByCount)
returnJson(obj, w, r)
groupQuery := database.ParseQueries(r, service.Failures())
objs, err := groupQuery.GraphData(database.ByCount)
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(objs, w, r)
}
func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
@ -154,10 +164,16 @@ func apiServicePingDataHandler(w http.ResponseWriter, r *http.Request) {
sendErrorJson(errors.New("service data not found"), w, r)
return
}
groupQuery := database.ParseQueries(r, service.Hits())
obj := core.GraphData(groupQuery, &types.Hit{}, database.ByAverage("ping_time"))
returnJson(obj, w, r)
objs, err := groupQuery.GraphData(database.ByAverage("ping_time"))
if err != nil {
sendErrorJson(err, w, r)
return
}
returnJson(objs, w, r)
}
type dataXy struct {
@ -190,10 +206,11 @@ func apiAllServicesHandler(r *http.Request) interface{} {
return joinServices(services)
}
func joinServices(srvs []database.Servicer) []*types.Service {
func joinServices(srvs []*core.Service) []*types.Service {
var services []*types.Service
for _, v := range srvs {
services = append(services, v.Model())
v.UpdateStats()
services = append(services, v.Service)
}
return services
}

View File

@ -52,7 +52,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
sample, _ := strconv.ParseBool(r.PostForm.Get("sample_data"))
dir := utils.Directory
config := &types.DbConfig{
config := &core.DbConfig{DbConfig: &types.DbConfig{
DbConn: dbConn,
DbHost: dbHost,
DbUser: dbUser,
@ -67,11 +67,11 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
Email: email,
Error: nil,
Location: utils.Directory,
}
}}
log.WithFields(utils.ToFields(core.CoreApp, config)).Debugln("new configs posted")
if _, err := core.CoreApp.SaveConfig(config); err != nil {
if err := config.Save(); err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
return
@ -100,7 +100,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
return
}
core.CoreApp, err = core.CoreApp.InsertCore(config)
core.CoreApp, err = config.InsertCore()
if err != nil {
log.Errorln(err)
sendErrorJson(err, w, r)
@ -130,7 +130,7 @@ func processSetupHandler(w http.ResponseWriter, r *http.Request) {
Config *types.DbConfig `json:"config"`
}{
"okokok",
config,
config.DbConfig,
}
returnJson(out, w, r)
}

View File

@ -17,6 +17,7 @@ package notifiers
import (
"github.com/hunterlong/statping/core/notifier"
"github.com/hunterlong/statping/database"
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
@ -80,5 +81,5 @@ func injectDatabase() {
panic(err)
}
db.CreateTable(&notifier.Notification{})
notifier.SetDB(&types.Db{db})
notifier.SetDB(&database.Db{db, "sqlite3"})
}

View File

@ -1,2 +0,0 @@
// Package plugin contains the interfaces to build your own Golang Plugin that will receive triggers on Statping events.
package plugin

View File

@ -1,116 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package plugin
import (
"fmt"
"github.com/hunterlong/statping/core"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"io/ioutil"
"os"
"plugin"
"strings"
)
//
// STATPING PLUGIN INTERFACE
//
// v0.1
//
// https://statping.com
//
//
// An expandable plugin framework that will still
// work even if there's an update or addition.
//
var (
AllPlugins []*types.PluginObject
dir string
log = utils.Log.WithField("type", "plugin")
)
func init() {
utils.InitLogs()
dir = utils.Directory
}
func LoadPlugin(file string) error {
log.Infoln(fmt.Sprintf("opening file %v", file))
f, err := os.Open(file)
if err != nil {
return err
}
fSplit := strings.Split(f.Name(), "/")
fileBin := fSplit[len(fSplit)-1]
log.Infoln(fmt.Sprintf("Attempting to load plugin '%v'", fileBin))
ext := strings.Split(fileBin, ".")
if len(ext) != 2 {
log.Errorln(fmt.Sprintf("Plugin '%v' must end in .so extension", fileBin))
return fmt.Errorf("Plugin '%v' must end in .so extension %v", fileBin, len(ext))
}
if ext[1] != "so" {
log.Errorln(fmt.Sprintf("Plugin '%v' must end in .so extension", fileBin))
return fmt.Errorf("Plugin '%v' must end in .so extension", fileBin)
}
plug, err := plugin.Open(file)
if err != nil {
log.Errorln(fmt.Sprintf("Plugin '%v' could not load correctly. %v", fileBin, err))
return err
}
symPlugin, err := plug.Lookup("Plugin")
if err != nil {
log.Errorln(fmt.Sprintf("Plugin '%v' could not locate Plugin variable. %v", fileBin, err))
return err
}
plugActions, ok := symPlugin.(types.PluginActions)
if !ok {
log.Errorln(fmt.Sprintf("Plugin %v was not type PluginObject", f.Name()))
return fmt.Errorf("Plugin %v was not type PluginActions %v", f.Name(), plugActions.GetInfo())
}
info := plugActions.GetInfo()
err = plugActions.OnLoad()
if err != nil {
return err
}
log.Infoln(fmt.Sprintf("Plugin %v loaded from %v", info.Name, f.Name()))
core.CoreApp.AllPlugins = append(core.CoreApp.AllPlugins, plugActions)
return nil
}
func LoadPlugins() {
pluginDir := dir + "/plugins"
log.Infoln(fmt.Sprintf("Loading any available Plugins from /plugins directory"))
if _, err := os.Stat(pluginDir); os.IsNotExist(err) {
os.Mkdir(pluginDir, os.ModePerm)
}
files, err := ioutil.ReadDir(pluginDir)
if err != nil {
log.Warnln(fmt.Sprintf("Plugins directory was not found. Error: %v", err))
return
}
for _, f := range files {
err := LoadPlugin(f.Name())
if err != nil {
log.Errorln(err)
continue
}
}
log.Infoln(fmt.Sprintf("Loaded %v Plugins", len(core.CoreApp.Plugins)))
}

View File

@ -1,51 +0,0 @@
// Statping
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statping
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package plugin
import (
"github.com/hunterlong/statping/source"
"github.com/hunterlong/statping/types"
"github.com/hunterlong/statping/utils"
"testing"
)
var (
example types.PluginActions
)
func init() {
utils.InitLogs()
source.Assets()
}
func TestLoadPlugin(t *testing.T) {
//err := LoadPlugin(dir+"/plugins/example.so")
//assert.Nil(t, err)
}
func TestAdd(t *testing.T) {
//err := Add(example)
//assert.NotNil(t, err)
}
func TestSelect(t *testing.T) {
//err := example.GetInfo()
//assert.Equal(t, "", err.Name)
}
//func TestAddRoute(t *testing.T) {
// example.AddRoute("/plugin_example", "GET", setupHandler)
//}

View File

@ -80,7 +80,9 @@ func UsingAssets(folder string) bool {
if _, err := os.Stat(folder + "/assets"); err == nil {
return true
} else {
if os.Getenv("USE_ASSETS") == "true" {
useAssets := utils.Getenv("USE_ASSETS", false).(bool)
if useAssets {
log.Infoln("Environment variable USE_ASSETS was found.")
if err := CreateAllAssets(folder); err != nil {
log.Warnln(err)

View File

@ -36,15 +36,6 @@ type Checkin struct {
Failures []*Failure `gorm:"-" json:"failures"`
}
// BeforeCreate for Checkin will set CreatedAt to UTC
func (c *Checkin) BeforeCreate() (err error) {
if c.CreatedAt.IsZero() {
c.CreatedAt = time.Now().UTC()
c.UpdatedAt = time.Now().UTC()
}
return
}
// CheckinHit is a successful response from a Checkin
type CheckinHit struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`

View File

@ -47,13 +47,15 @@ type Core struct {
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Started time.Time `gorm:"-" json:"started_on"`
Plugins []*Info `gorm:"-" json:"-"`
Repos []PluginJSON `gorm:"-" json:"-"`
AllPlugins []PluginActions `gorm:"-" json:"-"`
Notifications []AllNotifiers `gorm:"-" json:"-"`
Config *DbConfig `gorm:"-" json:"-"`
Integrations []Integrator `gorm:"-" json:"-"`
}
func (Core) TableName() string {
return "core"
}
type Servicer interface {
Model() *Service
}

90
types/errors.go Normal file
View File

@ -0,0 +1,90 @@
package types
import (
"github.com/pkg/errors"
"net/http"
)
var (
ErrorServiceSelection = returnErr("error selecting services")
// create errors
ErrorCreateService = returnErr("error creating service")
ErrorCreateMessage = returnErr("error creating messages")
ErrorCreateIncident = returnErr("error creating incident")
ErrorCreateUser = returnErr("error creating user")
ErrorCreateIncidentUp = returnErr("error creating incident update")
ErrorCreateGroup = returnErr("error creating group")
ErrorCreateCheckinHit = returnErr("error creating checkin hit")
ErrorCreateSampleHits = returnErr("error creating sample hits")
ErrorCreateCore = returnErr("error creating core")
ErrorCreateHit = returnErr("error creating hit for service %v")
ErrorDirCreate = returnErr("error creating directory %s")
ErrorFileCopy = returnErr("error copying file %s to %s")
ErrorConfig = returnErr("error with configuration")
ErrorConnection = returnErr("error with connection")
ErrorNotFound = returnErrCode("item was not found", http.StatusNotFound)
ErrorJSONParse = returnErrCode("could not parse JSON request", http.StatusBadRequest)
)
type Errorer interface {
}
type Error struct {
err error
code int
}
func (e Error) Error() string {
return e.err.Error()
}
func (e Error) String() string {
return e.err.Error()
}
func returnErrCode(str string, code int) error {
return Error{
err: errors.New(str),
code: code,
}
}
func returnErr(str string) Error {
return Error{
err: errors.New(str),
}
}
func convertError(val interface{}) string {
switch v := val.(type) {
case *Error:
return v.Error()
case string:
return v
default:
return ""
}
}
type errorer interface {
Error() string
}
func ErrWrap(err errorer, format interface{}, args ...interface{}) Error {
return Error{
err: errors.Wrapf(err, convertError(format), args...),
code: 0,
}
}
func Err(err errorer, format interface{}) Error {
return Error{
err: errors.Wrap(err, convertError(format)),
code: 0,
}
}

View File

@ -58,8 +58,8 @@ type Service struct {
SuccessNotified bool `gorm:"-" json:"-"` // Is 'true' if the user has already be informed that the Services now again available
LastStatusCode int `gorm:"-" json:"status_code"`
LastOnline time.Time `gorm:"-" json:"last_success"`
Failures []Failure `gorm:"-" json:"failures,omitempty" scope:"user,admin"`
Checkins []CheckinInterface `gorm:"-" json:"checkins,omitempty" scope:"user,admin"`
Failures []*Failure `gorm:"-" json:"failures,omitempty" scope:"user,admin"`
Checkins []*Checkin `gorm:"-" json:"checkins,omitempty" scope:"user,admin"`
Stats *Stats `gorm:"-" json:"stats,omitempty"`
}
@ -72,8 +72,8 @@ type Stater interface {
}
type Stats struct {
Failures uint64 `gorm:"-" json:"failures,omitempty"`
Hits uint64 `gorm:"-" json:"hits,omitempty"`
Failures int `gorm:"-" json:"failures"`
Hits int `gorm:"-" json:"hits"`
}
// BeforeCreate for Service will set CreatedAt to UTC
@ -85,6 +85,10 @@ func (s *Service) BeforeCreate() (err error) {
return
}
func (s *Service) Duration() time.Duration {
return time.Duration(s.Interval) * time.Second
}
// Start will create a channel for the service checking go routine
func (s *Service) Start() {
s.Running = make(chan bool)

View File

@ -44,29 +44,54 @@ var (
// init will set the utils.Directory to the current running directory, or STATPING_DIR if it is set
func init() {
if os.Getenv("STATPING_DIR") != "" {
Directory = os.Getenv("STATPING_DIR")
} else {
dir, err := os.Getwd()
defaultDir, err := os.Getwd()
if err != nil {
Directory = "."
return
}
Directory = dir
defaultDir = "."
}
Directory = Getenv("STATPING_DIR", defaultDir).(string)
// check if logs are disabled
logger := os.Getenv("DISABLE_LOGS")
disableLogs, _ = strconv.ParseBool(logger)
disableLogs = Getenv("DISABLE_LOGS", false).(bool)
if disableLogs {
Log.Out = ioutil.Discard
return
}
Log.Debugln("current working directory: ", Directory)
Log.AddHook(new(hook))
Log.SetNoLock()
checkVerboseMode()
}
func Getenv(key string, defaultValue interface{}) interface{} {
if val, ok := os.LookupEnv(key); ok {
if val != "" {
switch d := defaultValue.(type) {
case int, int64:
return int(ToInt(val))
case time.Duration:
dur, err := time.ParseDuration(val)
if err != nil {
return d
}
return dur
case bool:
ok, err := strconv.ParseBool(val)
if err != nil {
return d
}
return ok
default:
return val
}
}
}
return defaultValue
}
func SliceConvert(g []*interface{}) []interface{} {
var arr []interface{}
for _, v := range g {