fixed notification - additional tests - api changes

pull/78/head
Hunter Long 2018-09-14 18:18:21 -07:00
parent d24156f005
commit 36c912d23a
44 changed files with 1581 additions and 985 deletions

34
Gopkg.lock generated
View File

@ -63,14 +63,6 @@
pruneopts = "UT"
revision = "1eb28afdf9b6e56cf673badd47545f844fe81103"
[[projects]]
digest = "1:ca82a3b99694824c627573c2a76d0e49719b4a9c02d1d85a2ac91f1c1f52ab9b"
name = "github.com/fatih/structs"
packages = ["."]
pruneopts = "UT"
revision = "a720dfa8df582c51dee1b36feabb906bde1588bd"
version = "v1.0"
[[projects]]
digest = "1:64a5a67c69b70c2420e607a8545d674a23778ed9c3e80607bfd17b77c6c87f6a"
name = "github.com/go-ole/go-ole"
@ -169,12 +161,12 @@
revision = "04140366298a54a039076d798123ffa108fff46c"
[[projects]]
digest = "1:70e697d67ccaec45e16bac3a32380ebcd9e7e071079c60d0171d42cf1cf9748a"
digest = "1:ecd9aa82687cf31d1585d4ac61d0ba180e42e8a6182b85bd785fcca8dfeefc1b"
name = "github.com/joho/godotenv"
packages = ["."]
pruneopts = "UT"
revision = "a79fa1e548e2c689c241d10173efd51e5d689d5b"
version = "v1.2.0"
revision = "23d116af351c84513e1946b527c88823e476be13"
version = "v1.3.0"
[[projects]]
branch = "master"
@ -262,6 +254,14 @@
pruneopts = "UT"
revision = "bb4de0191aa41b5507caa14b0650cdbddcd9280b"
[[projects]]
branch = "master"
digest = "1:def689e73e9252f6f7fe66834a76751a41b767e03daab299e607e7226c58a855"
name = "github.com/shurcooL/sanitized_anchor_name"
packages = ["."]
pruneopts = "UT"
revision = "86672fcb3f950f35f2e675df2240550f2a50762f"
[[projects]]
digest = "1:18752d0b95816a1b777505a97f71c7467a8445b8ffb55631a7bf779f6ba4fa83"
name = "github.com/stretchr/testify"
@ -280,7 +280,7 @@
"md4",
]
pruneopts = "UT"
revision = "0709b304e793a5edb4a2c0145f281ecdc20838a4"
revision = "0e37d006457bf46f9e6692014ba72ef82c33022c"
[[projects]]
branch = "master"
@ -325,6 +325,14 @@
revision = "a96e63847dc3c67d17befa69c303767e2f84e54f"
version = "v2.1"
[[projects]]
digest = "1:39c2113f3a89585666e6f973650cff186b2d06deb4aa202c88addb87b0a201db"
name = "gopkg.in/russross/blackfriday.v2"
packages = ["."]
pruneopts = "UT"
revision = "cadec560ec52d93835bf2f15bd794700d3a2473b"
version = "v2.0.0"
[[projects]]
digest = "1:0a6ab450a46e158a97e3daf7da9df3bfd4f84420047fab6a65fb70b1337ce026"
name = "upper.io/db.v3"
@ -349,7 +357,6 @@
"github.com/GeertJohan/go.rice",
"github.com/GeertJohan/go.rice/embedded",
"github.com/ararog/timeago",
"github.com/fatih/structs",
"github.com/go-yaml/yaml",
"github.com/gorilla/handlers",
"github.com/gorilla/mux",
@ -367,6 +374,7 @@
"golang.org/x/crypto/bcrypt",
"gopkg.in/gomail.v2",
"gopkg.in/natefinch/lumberjack.v2",
"gopkg.in/russross/blackfriday.v2",
"upper.io/db.v3/lib/sqlbuilder",
]
solver-name = "gps-cdcl"

View File

@ -33,10 +33,6 @@
name = "github.com/ararog/timeago"
version = "0.0.1"
[[constraint]]
name = "github.com/fatih/structs"
version = "1.0.0"
[[constraint]]
name = "github.com/go-yaml/yaml"
version = "2.2.1"
@ -59,7 +55,7 @@
[[constraint]]
name = "github.com/joho/godotenv"
version = "1.2.0"
version = "1.3.0"
[[constraint]]
branch = "master"
@ -89,6 +85,10 @@
name = "gopkg.in/natefinch/lumberjack.v2"
version = "2.1.0"
[[constraint]]
name = "gopkg.in/russross/blackfriday.v2"
version = "2.0.0"
[[constraint]]
name = "upper.io/db.v3"
version = "3.5.4"

View File

@ -1,4 +1,4 @@
VERSION=0.59
VERSION=0.6
BINARY_NAME=statup
GOPATH:=$(GOPATH)
GOCMD=go

View File

@ -20,6 +20,7 @@ import (
"fmt"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/handlers"
_ "github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/utils"
"github.com/joho/godotenv"

View File

@ -56,7 +56,6 @@ func RunInit(t *testing.T) {
source.Assets()
Clean()
route = handlers.Router()
LoadDotEnvs()
core.CoreApp = core.NewCore()
}
@ -111,7 +110,6 @@ func TestRunAll(t *testing.T) {
RunSelectAllMysqlServices(t)
})
t.Run(dbt+" Select Comms", func(t *testing.T) {
t.SkipNow()
RunSelectAllNotifiers(t)
})
t.Run(dbt+" Create Users", func(t *testing.T) {
@ -256,8 +254,8 @@ func RunDatabaseMigrations(t *testing.T, db string) {
}
func RunInsertSampleData(t *testing.T) {
core.Configs.SeedDatabase()
//assert.Nil(t, err)
err := core.InsertLargeSampleData()
assert.Nil(t, err)
}
func RunLoadConfig(t *testing.T) {
@ -281,7 +279,7 @@ func RunSelectCoreMYQL(t *testing.T, db string) {
}
assert.Nil(t, err)
t.Log("core: ", core.CoreApp.Core)
assert.Equal(t, "Awesome Status", core.CoreApp.Name)
assert.Equal(t, "Statup Sample Data", core.CoreApp.Name)
assert.Equal(t, db, core.CoreApp.DbConnection)
assert.NotEmpty(t, core.CoreApp.ApiKey)
assert.NotEmpty(t, core.CoreApp.ApiSecret)
@ -290,24 +288,23 @@ func RunSelectCoreMYQL(t *testing.T, db string) {
func RunSelectAllMysqlServices(t *testing.T) {
var err error
t.SkipNow()
services, err := core.CoreApp.SelectAllServices()
assert.Nil(t, err)
assert.Equal(t, 18, len(services))
assert.Equal(t, 15, len(services))
}
func RunSelectAllNotifiers(t *testing.T) {
var err error
notifier.SetDB(core.DbSession)
comms := notifier.Load()
core.CoreApp.Notifications = notifier.Load()
assert.Nil(t, err)
assert.Equal(t, 3, len(comms))
assert.Equal(t, 5, len(core.CoreApp.Notifications))
}
func RunUser_SelectAll(t *testing.T) {
users, err := core.SelectAllUsers()
assert.Nil(t, err)
assert.Equal(t, 3, len(users))
assert.Equal(t, 4, len(users))
}
func RunUser_Create(t *testing.T) {
@ -319,7 +316,7 @@ func RunUser_Create(t *testing.T) {
})
id, err := user.Create()
assert.Nil(t, err)
assert.Equal(t, int64(2), id)
assert.Equal(t, int64(3), id)
user2 := core.ReturnUser(&types.User{
Username: "superadmin",
Password: "admin",
@ -328,7 +325,7 @@ func RunUser_Create(t *testing.T) {
})
id, err = user2.Create()
assert.Nil(t, err)
assert.Equal(t, int64(3), id)
assert.Equal(t, int64(4), id)
}
func RunUser_Update(t *testing.T) {
@ -365,7 +362,7 @@ func RunSelectAllServices(t *testing.T) {
var err error
services, err := core.CoreApp.SelectAllServices()
assert.Nil(t, err)
assert.Equal(t, 18, len(services))
assert.Equal(t, 15, len(services))
for _, s := range services {
assert.NotEmpty(t, s.CreatedAt)
}
@ -374,7 +371,6 @@ func RunSelectAllServices(t *testing.T) {
func RunOneService_Check(t *testing.T) {
service := core.SelectService(1)
assert.NotNil(t, service)
t.Log(service)
assert.Equal(t, "Google", service.Name)
}
@ -389,9 +385,9 @@ func RunService_Create(t *testing.T) {
Method: "GET",
Timeout: 30,
})
id, err := service.Create()
id, err := service.Create(false)
assert.Nil(t, err)
assert.Equal(t, int64(19), id)
assert.Equal(t, int64(16), id)
}
func RunService_ToJSON(t *testing.T) {
@ -409,20 +405,27 @@ func RunService_AvgTime(t *testing.T) {
}
func RunService_Online24(t *testing.T) {
var dayAgo = time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := core.SelectService(1)
assert.NotNil(t, service)
online := service.OnlineSince(SERVICE_SINCE)
online := service.OnlineSince(dayAgo)
assert.NotEqual(t, float32(0), online)
service = core.SelectService(6)
assert.NotNil(t, service)
online = service.OnlineSince(SERVICE_SINCE)
assert.Equal(t, float32(0), online)
online = service.OnlineSince(dayAgo)
assert.Equal(t, float32(100), online)
//service = core.SelectService(18)
//assert.NotNil(t, service)
//online = service.OnlineSince(SERVICE_SINCE)
//assert.Equal(t, float32(0), online)
service = core.SelectService(13)
assert.NotNil(t, service)
online = service.OnlineSince(dayAgo)
assert.True(t, online > 99)
service = core.SelectService(14)
assert.NotNil(t, service)
online = service.OnlineSince(dayAgo)
assert.True(t, online > float32(49.00))
}
func RunService_GraphData(t *testing.T) {
@ -446,15 +449,15 @@ func RunBadService_Create(t *testing.T) {
Method: "GET",
Timeout: 30,
})
id, err := service.Create()
id, err := service.Create(false)
assert.Nil(t, err)
assert.Equal(t, int64(20), id)
assert.Equal(t, int64(17), id)
}
func RunBadService_Check(t *testing.T) {
service := core.SelectService(18)
service := core.SelectService(17)
assert.NotNil(t, service)
assert.Equal(t, "Failing URL", service.Name)
assert.Equal(t, "Bad Service", service.Name)
for i := 0; i <= 10; i++ {
service.Check(true)
}
@ -474,7 +477,7 @@ func RunDeleteService(t *testing.T) {
func RunCreateService_Hits(t *testing.T) {
services := core.CoreApp.Services
assert.NotNil(t, services)
assert.Equal(t, 19, len(services))
assert.Equal(t, 16, len(services))
for _, service := range services {
service.Check(true)
assert.NotNil(t, service)
@ -490,9 +493,9 @@ func RunService_Hits(t *testing.T) {
}
func RunService_Failures(t *testing.T) {
service := core.SelectService(18)
service := core.SelectService(17)
assert.NotNil(t, service)
assert.Equal(t, "Failing URL", service.Name)
assert.Equal(t, "Bad Service", service.Name)
assert.NotEmpty(t, service.AllFailures())
}
@ -509,7 +512,7 @@ func RunIndexHandler(t *testing.T) {
assert.Nil(t, err)
rr := httptest.NewRecorder()
route.ServeHTTP(rr, req)
assert.True(t, strings.Contains(rr.Body.String(), "Awesome"))
assert.True(t, strings.Contains(rr.Body.String(), "Statup"))
assert.True(t, strings.Contains(rr.Body.String(), "footer"))
}
@ -529,7 +532,7 @@ func RunPrometheusHandler(t *testing.T) {
rr := httptest.NewRecorder()
route.ServeHTTP(rr, req)
t.Log(rr.Body.String())
assert.True(t, strings.Contains(rr.Body.String(), "statup_total_services 19"))
assert.True(t, strings.Contains(rr.Body.String(), "statup_total_services 16"))
assert.True(t, handlers.IsAuthenticated(req))
}

View File

@ -30,8 +30,8 @@ import (
"time"
)
// CheckServices will start the checking go routine for each service
func CheckServices() {
// checkServices will start the checking go routine for each service
func checkServices() {
utils.Log(1, fmt.Sprintf("Starting monitoring process for %v Services", len(CoreApp.Services)))
for _, ser := range CoreApp.Services {
//go obj.StartCheckins()

View File

@ -115,7 +115,7 @@ func LoadUsingEnv() (*DbConfig, error) {
}
admin.Create()
LoadSampleData()
InsertSampleData()
return Configs, err
@ -123,3 +123,15 @@ func LoadUsingEnv() (*DbConfig, error) {
return Configs, nil
}
// DeleteConfig will delete the 'config.yml' file
func DeleteConfig() {
err := os.Remove(utils.Directory + "/config.yml")
if err != nil {
utils.Log(3, err)
}
}
type ErrorResponse struct {
Error string
}

View File

@ -17,7 +17,6 @@ package core
import (
"github.com/hunterlong/statup/core/notifier"
_ "github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
@ -60,7 +59,7 @@ func InitApp() {
SelectCore()
insertNotifierDB()
CoreApp.SelectAllServices()
CheckServices()
checkServices()
CoreApp.Notifications = notifier.Load()
go DatabaseMaintence()
}

View File

@ -86,7 +86,7 @@ func TestMigrateDatabase(t *testing.T) {
}
func TestSeedDatabase(t *testing.T) {
_, _, err := Configs.SeedDatabase()
err := InsertLargeSampleData()
assert.Nil(t, err)
}
@ -99,7 +99,7 @@ func TestReLoadDbConfig(t *testing.T) {
func TestSelectCore(t *testing.T) {
core, err := SelectCore()
assert.Nil(t, err)
assert.Equal(t, "Awesome Status", core.Name)
assert.Equal(t, "Statup Sample Data", core.Name)
}
func TestInsertNotifierDB(t *testing.T) {

View File

@ -232,21 +232,21 @@ func (c *DbConfig) CreateCore() *Core {
}
// SeedDatabase will insert many elements into the database. This is only ran in Dev/Test move
func (db *DbConfig) SeedDatabase() (string, string, error) {
utils.Log(1, "Seeding Database with Dummy Data...")
dir := utils.Directory
var cmd string
switch db.DbConn {
case "sqlite":
cmd = fmt.Sprintf("cat %v/dev/sqlite_seed.sql | sqlite3 %v/statup.db", dir, dir)
case "mysql":
cmd = fmt.Sprintf("mysql -h %v -P %v -u %v --password=%v %v < %v/dev/mysql_seed.sql", Configs.DbHost, 3306, Configs.DbUser, Configs.DbPass, Configs.DbData, dir)
case "postgres":
cmd = fmt.Sprintf("PGPASSWORD=%v psql -U %v -h %v -d %v -1 -f %v/dev/postgres_seed.sql", db.DbPass, db.DbUser, db.DbHost, db.DbData, dir)
}
out, outErr, err := utils.Command(cmd)
return out, outErr, err
}
//func (db *DbConfig) SeedDatabase() (string, string, error) {
// utils.Log(1, "Seeding Database with Dummy Data...")
// dir := utils.Directory
// var cmd string
// switch db.DbConn {
// case "sqlite":
// cmd = fmt.Sprintf("cat %v/dev/sqlite_seed.sql | sqlite3 %v/statup.db", dir, dir)
// case "mysql":
// cmd = fmt.Sprintf("mysql -h %v -P %v -u %v --password=%v %v < %v/dev/mysql_seed.sql", Configs.DbHost, 3306, Configs.DbUser, Configs.DbPass, Configs.DbData, dir)
// case "postgres":
// cmd = fmt.Sprintf("PGPASSWORD=%v psql -U %v -h %v -d %v -1 -f %v/dev/postgres_seed.sql", db.DbPass, db.DbUser, db.DbHost, db.DbData, dir)
// }
// out, outErr, err := utils.Command(cmd)
// return out, outErr, err
//}
// DropDatabase will DROP each table Statup created
func (db *DbConfig) DropDatabase() error {

View File

@ -26,7 +26,7 @@ var (
)
func checkNotifierForm(n Notifier) error {
notifier := n.Select()
notifier := asNotification(n)
for _, f := range notifier.Form {
contains := contains(f.DbField, allowed_vars)
if !contains {

View File

@ -21,9 +21,9 @@ import "github.com/hunterlong/statup/types"
func OnSave(method string) {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
notifier := comm.(Notifier).Select()
if notifier.Method == method {
comm.(Notifier).OnSave()
notifier := comm.(Notifier)
if notifier.Select().Method == method {
notifier.OnSave()
}
}
}
@ -32,7 +32,7 @@ func OnSave(method string) {
// OnFailure will be triggered when a service is failing - BasicEvents interface
func OnFailure(s *types.Service, f *types.Failure) {
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(BasicEvents).OnFailure(s, f)
}
}
@ -41,7 +41,7 @@ func OnFailure(s *types.Service, f *types.Failure) {
// OnSuccess will be triggered when a service is successful - BasicEvents interface
func OnSuccess(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(BasicEvents)) && isEnabled(comm) {
if isType(comm, new(BasicEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(BasicEvents).OnSuccess(s)
}
}
@ -50,7 +50,7 @@ func OnSuccess(s *types.Service) {
// OnNewService is triggered when a new service is created - ServiceEvents interface
func OnNewService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(ServiceEvents).OnNewService(s)
}
}
@ -59,7 +59,7 @@ func OnNewService(s *types.Service) {
// OnUpdatedService is triggered when a service is updated - ServiceEvents interface
func OnUpdatedService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(ServiceEvents).OnUpdatedService(s)
}
}
@ -68,7 +68,7 @@ func OnUpdatedService(s *types.Service) {
// OnDeletedService is triggered when a service is deleted - ServiceEvents interface
func OnDeletedService(s *types.Service) {
for _, comm := range AllCommunications {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) {
if isType(comm, new(ServiceEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(ServiceEvents).OnDeletedService(s)
}
}
@ -77,7 +77,7 @@ func OnDeletedService(s *types.Service) {
// OnNewUser is triggered when a new user is created - UserEvents interface
func OnNewUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(UserEvents).OnNewUser(u)
}
}
@ -86,7 +86,7 @@ func OnNewUser(u *types.User) {
// OnUpdatedUser is triggered when a new user is updated - UserEvents interface
func OnUpdatedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(UserEvents).OnUpdatedUser(u)
}
}
@ -95,7 +95,7 @@ func OnUpdatedUser(u *types.User) {
// OnDeletedUser is triggered when a new user is deleted - UserEvents interface
func OnDeletedUser(u *types.User) {
for _, comm := range AllCommunications {
if isType(comm, new(UserEvents)) && isEnabled(comm) {
if isType(comm, new(UserEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(UserEvents).OnDeletedUser(u)
}
}
@ -104,7 +104,7 @@ func OnDeletedUser(u *types.User) {
// OnUpdatedCore is triggered when the CoreApp settings are saved - CoreEvents interface
func OnUpdatedCore(c *types.Core) {
for _, comm := range AllCommunications {
if isType(comm, new(CoreEvents)) && isEnabled(comm) {
if isType(comm, new(CoreEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(CoreEvents).OnUpdatedCore(c)
}
}
@ -113,7 +113,7 @@ func OnUpdatedCore(c *types.Core) {
// NotifierEvents interface
func OnNewNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(NotifierEvents).OnNewNotifier(n)
}
}
@ -122,7 +122,7 @@ func OnNewNotifier(n *Notification) {
// NotifierEvents interface
func OnUpdatedNotifier(n *Notification) {
for _, comm := range AllCommunications {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) {
if isType(comm, new(NotifierEvents)) && isEnabled(comm) && inLimits(comm) {
comm.(NotifierEvents).OnUpdatedNotifier(n)
}
}

View File

@ -16,26 +16,30 @@
package notifier
import (
"errors"
"fmt"
"github.com/hunterlong/statup/types"
"time"
)
type Example struct {
*Notification
}
const (
EXAMPLE_METHOD = "example"
)
var example = &Example{&Notification{
Method: EXAMPLE_METHOD,
Host: "http://exmaplehost.com",
Method: METHOD,
Host: "http://exmaplehost.com",
Title: "Example",
Description: "Example Notifier",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(5 * time.Second),
Form: []NotificationForm{{
Type: "text",
Title: "Host",
Placeholder: "Insert your Host here.",
DbField: "host",
SmallText: "this is where you would put the host",
}, {
Type: "text",
Title: "Username",
@ -71,8 +75,8 @@ var example = &Example{&Notification{
Title: "Var2",
Placeholder: "Var2 goes here",
DbField: "var2",
}}},
}
}},
}}
// REQUIRED init() will install/load the notifier
func init() {
@ -80,17 +84,9 @@ func init() {
}
// REQUIRED
func (n *Example) Run() error {
return nil
}
// REQUIRED
func (n *Example) OnSave() error {
return nil
}
// REQUIRED
func (n *Example) Test() error {
func (n *Example) Send(msg interface{}) error {
message := msg.(string)
fmt.Printf("i received this string: %v\n", message)
return nil
}
@ -99,62 +95,82 @@ func (n *Example) Select() *Notification {
return n.Notification
}
// REQUIRED
func (n *Example) OnSave() error {
msg := fmt.Sprintf("received on save trigger")
n.AddQueue(msg)
return errors.New("onsave triggered")
}
// REQUIRED
func (n *Example) Test() error {
msg := fmt.Sprintf("received a test trigger\n")
n.AddQueue(msg)
return errors.New("test triggered")
}
// REQUIRED - BASIC EVENT
func (n *Example) OnSuccess(s *types.Service) {
saySomething("service is is online!")
msg := fmt.Sprintf("received a count trigger for service: %v\n", s.Name)
n.AddQueue(msg)
}
// REQUIRED - BASIC EVENT
func (n *Example) OnFailure(s *types.Service, f *types.Failure) {
saySomething("service is failing!")
}
// Example function to do something awesome or not...
func saySomething(text ...interface{}) {
fmt.Println(text)
msg := fmt.Sprintf("received a failure trigger for service: %v\n", s.Name)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnNewService(s *types.Service) {
msg := fmt.Sprintf("received a new service trigger for service: %v\n", s.Name)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnUpdatedService(s *types.Service) {
msg := fmt.Sprintf("received a update service trigger for service: %v\n", s.Name)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnDeletedService(s *types.Service) {
msg := fmt.Sprintf("received a delete service trigger for service: %v\n", s.Name)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnNewUser(s *types.User) {
msg := fmt.Sprintf("received a new user trigger for user: %v\n", s.Username)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnUpdatedUser(s *types.User) {
msg := fmt.Sprintf("received a updated user trigger for user: %v\n", s.Username)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnDeletedUser(s *types.User) {
msg := fmt.Sprintf("received a deleted user trigger for user: %v\n", s.Username)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnUpdatedCore(s *types.Core) {
msg := fmt.Sprintf("received a updated core trigger for core: %v\n", s.Name)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnNewNotifier(s *Notification) {
msg := fmt.Sprintf("received a new notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(msg)
}
// OPTIONAL
func (n *Example) OnUpdatedNotifier(s *Notification) {
msg := fmt.Sprintf("received a update notifier trigger for notifier: %v\n", s.Method)
n.AddQueue(msg)
}

View File

@ -19,10 +19,10 @@ import "github.com/hunterlong/statup/types"
// Notifier interface is required to create a new Notifier
type Notifier interface {
Run() error // Run will trigger inside of the notifier when enabled
OnSave() error // OnSave is triggered when the notifier is saved
Test() error // Test will run a function inside the notifier to Test if it works
Select() *Notification // Select returns the *Notification for a notifier
OnSave() error // OnSave is triggered when the notifier is saved
Send(interface{}) error // OnSave is triggered when the notifier is saved
Test() error // Test will run a function inside the notifier to Test if it works
Select() *Notification // Select returns the *Notification for a notifier
}
// BasicEvents includes the most minimal events, failing and successful service triggers

View File

@ -16,6 +16,7 @@
package notifier
import (
"encoding/json"
"errors"
"fmt"
"github.com/hunterlong/statup/types"
@ -32,24 +33,29 @@ var (
)
type Notification struct {
Id int64 `gorm:"primary_key;column:id" json:"id"`
Method string `gorm:"column:method" json:"method"`
Host string `gorm:"not null;column:host" json:"-"`
Port int `gorm:"not null;column:port" json:"-"`
Username string `gorm:"not null;column:username" json:"-"`
Password string `gorm:"not null;column:password" json:"-"`
Var1 string `gorm:"not null;column:var1" json:"-"`
Var2 string `gorm:"not null;column:var2" json:"-"`
ApiKey string `gorm:"not null;column:api_key" json:"-"`
ApiSecret string `gorm:"not null;column:api_secret" json:"-"`
Enabled bool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Limits int `gorm:"not null;column:limits" json:"-"`
Removable bool `gorm:"column:removable" json:"-"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Form []NotificationForm `gorm:"-" json:"-"`
Routine chan struct{} `gorm:"-" json:"-"`
logs []*NotificationLog `gorm:"-" json:"-"`
Id int64 `gorm:"primary_key;column:id" json:"id"`
Method string `gorm:"column:method" json:"method"`
Host string `gorm:"not null;column:host" json:"-"`
Port int `gorm:"not null;column:port" json:"-"`
Username string `gorm:"not null;column:username" json:"-"`
Password string `gorm:"not null;column:password" json:"-"`
Var1 string `gorm:"not null;column:var1" json:"-"`
Var2 string `gorm:"not null;column:var2" json:"-"`
ApiKey string `gorm:"not null;column:api_key" json:"-"`
ApiSecret string `gorm:"not null;column:api_secret" json:"-"`
Enabled bool `gorm:"column:enabled;type:boolean;default:false" json:"enabled"`
Limits int `gorm:"not null;column:limits" json:"-"`
Removable bool `gorm:"column:removable" json:"-"`
CreatedAt time.Time `gorm:"column:created_at" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at" json:"updated_at"`
Form []NotificationForm `gorm:"-" json:"-"`
logs []*NotificationLog `gorm:"-" json:"-"`
Title string `gorm:"-" json:"-"`
Description string `gorm:"-" json:"-"`
Author string `gorm:"-" json:"-"`
AuthorUrl string `gorm:"-" json:"-"`
Delay time.Duration `gorm:"-" json:"-"`
Queue []interface{} `gorm:"-" json:"-"`
}
type NotificationForm struct {
@ -57,6 +63,7 @@ type NotificationForm struct {
Title string
Placeholder string
DbField string
SmallText string
}
type NotificationLog struct {
@ -65,24 +72,40 @@ type NotificationLog struct {
Timestamp time.Time
}
func (n *Notification) AddQueue(msg interface{}) {
n.Queue = append(n.Queue, msg)
}
// db will return the notifier database column/record
func (n *Notification) db() *gorm.DB {
func modelDb(n *Notification) *gorm.DB {
return db.Model(&Notification{}).Where("method = ?", n.Method).Find(n)
}
func toNotification(n Notifier) *Notification {
return n.Select()
}
// SetDB is called by core to inject the database for a notifier to use
func SetDB(d *gorm.DB) {
db = d
}
func asNotifier(n interface{}) Notifier {
return n.(Notifier)
}
func asNotification(n interface{}) *Notification {
return n.(Notifier).Select()
}
// AddNotifier accept a Notifier interface to be added into the array
func AddNotifier(c Notifier) error {
if notifier, ok := c.(Notifier); ok {
err := checkNotifierForm(notifier)
func AddNotifier(n interface{}) error {
if isType(n, new(Notifier)) {
err := checkNotifierForm(asNotifier(n))
if err != nil {
return err
}
AllCommunications = append(AllCommunications, notifier)
AllCommunications = append(AllCommunications, n)
} else {
return errors.New("notifier does not have the required methods")
}
@ -96,19 +119,46 @@ func Load() []types.AllNotifiers {
n := comm.(Notifier)
Init(n)
notifiers = append(notifiers, n)
//n.Test()
}
startAllNotifiers()
return notifiers
}
func (n *Notification) Select() *Notification {
return n
func normalizeType(ty interface{}) string {
switch v := ty.(type) {
case int, int32, int64:
return fmt.Sprintf("%v", v)
case float32, float64:
return fmt.Sprintf("%v", v)
case string:
return v
case []byte:
return string(v)
case []string:
return fmt.Sprintf("%v", v)
case interface{}, map[string]interface{}:
j, _ := json.Marshal(v)
return string(j)
default:
return fmt.Sprintf("%v", v)
}
}
func (n *Notification) removeQueue(msg interface{}) interface{} {
var newArr []interface{}
for _, q := range n.Queue {
if q != msg {
newArr = append(newArr, q)
}
}
n.Queue = newArr
return newArr
}
// Log will record a new notification into memory and will show the logs on the settings page
func (n *Notification) Log(msg string) {
func (n *Notification) Log(msg interface{}) {
log := &NotificationLog{
Message: msg,
Message: normalizeType(msg),
Time: utils.Timestamp(time.Now()),
Timestamp: time.Now(),
}
@ -129,21 +179,21 @@ func reverseLogs(input []*NotificationLog) []*NotificationLog {
}
// isInDatabase returns true if the notifier has already been installed
func (n *Notification) isInDatabase() bool {
inDb := n.db().RecordNotFound()
func isInDatabase(n *Notification) bool {
inDb := modelDb(n).RecordNotFound()
return !inDb
}
// SelectNotification returns the Notification struct from the database
func SelectNotification(method string) (*Notification, error) {
var notifier Notification
err := db.Model(&Notification{}).Where("method = ?", method).Scan(&notifier)
return &notifier, err.Error
func SelectNotification(n Notifier) (*Notification, error) {
notifier := n.Select()
err := db.Model(&Notification{}).Where("method = ?", notifier.Method).Scan(&notifier)
return notifier, err.Error
}
// Update will update the notification into the database
func (n *Notification) Update() (*Notification, error) {
err := n.db().Update(n)
err := db.Model(&Notification{}).Update(n)
return n, err.Error
}
@ -172,30 +222,62 @@ func SelectNotifier(method string) (*Notification, error) {
return nil, nil
}
// CanSend will return true if notifier has not passed its Limits within the last hour
func (f *Notification) CanSend() bool {
if f.SentLastHour() >= f.Limits {
return false
}
return true
}
// Init accepts the Notifier interface to initialize the notifier
func Init(n Notifier) (*Notification, error) {
err := install(n)
var notify *Notification
if err == nil {
notify, _ = SelectNotification(n.Select().Method)
notify.Form = n.Select().Form
notify, _ = SelectNotification(n)
notify.Form = toNotification(n).Form
}
return notify, err
}
func startAllNotifiers() {
for _, comm := range AllCommunications {
if isType(comm, new(Notifier)) {
if toNotification(comm.(Notifier)).Enabled {
go runQue(comm.(Notifier))
}
}
}
}
func runQue(n Notifier) {
for {
notification := n.Select()
if len(notification.Queue) > 0 {
for _, msg := range notification.Queue {
if notification.WithinLimits() {
err := n.Send(msg)
if err != nil {
utils.Log(2, fmt.Sprintf("notifier %v had an error: %v", notification.Method, err))
}
notification.Log(msg)
}
}
}
time.Sleep(notification.Delay)
}
}
func RunQue(n Notifier) error {
notifier := n.Select()
if len(notifier.Queue) == 0 {
return nil
}
queMsg := notifier.Queue[0]
err := n.Send(queMsg)
notifier.Log(queMsg)
notifier.Queue = notifier.Queue[1:]
return err
}
// install will check the database for the notification, if its not inserted it will insert a new record for it
func install(n Notifier) error {
inDb := n.Select().isInDatabase()
inDb := isInDatabase(n.Select())
if !inDb {
_, err := insertDatabase(n.Select())
_, err := insertDatabase(toNotification(n))
if err != nil {
utils.Log(3, err)
return err
@ -228,8 +310,8 @@ func (f *Notification) SentLastHour() int {
}
// Limit returns the limits on how many notifications can be sent in 1 hour
func (f *Notification) Limit() int64 {
return utils.StringInt(f.GetValue("limits"))
func (f *Notification) Limit() int {
return f.Limits
}
// GetValue returns the database value of a accept DbField value.
@ -262,33 +344,31 @@ func (n *Notification) GetValue(dbField string) string {
// isType will return true if a variable can implement an interface
func isType(n interface{}, obj interface{}) bool {
objOne := reflect.TypeOf(n)
obj2 := reflect.TypeOf(obj)
return objOne.String() == obj2.String()
one := reflect.TypeOf(n)
two := reflect.ValueOf(obj).Elem()
return one.Implements(two.Type())
}
// isEnabled returns true if the notifier is enabled
func isEnabled(n interface{}) bool {
notify := n.(Notifier).Select()
return notify.Enabled
notifier, _ := SelectNotification(n.(Notifier))
return notifier.Enabled
}
func UniqueStrings(elements []string) []string {
result := []string{}
func inLimits(n interface{}) bool {
notifier := toNotification(n.(Notifier))
return notifier.WithinLimits()
}
for i := 0; i < len(elements); i++ {
// Scan slice for a previous element of the same value.
exists := false
for v := 0; v < i; v++ {
if elements[v] == elements[i] {
exists = true
break
}
}
// If no previous element exists, append this one.
if !exists {
result = append(result, elements[i])
}
func (notify *Notification) WithinLimits() bool {
if notify.SentLastHour() >= notify.Limit() {
return false
}
return result
if notify.Delay.Seconds() == 0 {
notify.Delay = time.Duration(2 * time.Second)
}
if notify.LastSent().Seconds() >= notify.Delay.Seconds() {
return false
}
return true
}

View File

@ -1,5 +1,3 @@
// +build bypass
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
@ -25,105 +23,222 @@ import (
_ "github.com/jinzhu/gorm/dialects/sqlite"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
var (
dir string
EXAMPLE_ID = "example"
dir string
METHOD = "example"
)
var service = &types.Service{
Name: "Interpol - All The Rage Back Home",
Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
}
var failure = &types.Failure{
Issue: "testing",
}
var user = &types.User{
Username: "admin",
Email: "info@email.com",
}
var core = &types.Core{
Name: "testing notifiers",
}
func init() {
dir = utils.Directory
}
func injectDatabase() {
db, _ = gorm.Open("sqlite3", dir+"/statup.db")
}
func TestLoad(t *testing.T) {
source.Assets()
utils.InitLogs()
injectDatabase()
AllCommunications = Load()
assert.Len(t, AllCommunications, 1)
}
func injectDatabase() {
utils.DeleteFile(dir + "/statup.db")
db, _ = gorm.Open("sqlite3", dir+"/statup.db")
db.CreateTable(&Notification{})
}
func TestIsBasicType(t *testing.T) {
assert.True(t, isType(example, new(Notifier)))
assert.True(t, isType(example, new(BasicEvents)))
assert.True(t, isType(example, new(ServiceEvents)))
assert.True(t, isType(example, new(UserEvents)))
assert.True(t, isType(example, new(CoreEvents)))
assert.True(t, isType(example, new(NotifierEvents)))
}
func TestLoad(t *testing.T) {
notifiers := Load()
assert.Equal(t, 1, len(notifiers))
}
func TestIsInDatabase(t *testing.T) {
in := example.isInDatabase()
assert.True(t, in)
}
func TestInsertDatabase(t *testing.T) {
_, err := insertDatabase(example.Notification)
assert.Nil(t, err)
assert.NotZero(t, example.Id)
in := example.isInDatabase()
in := isInDatabase(example.Notification)
assert.True(t, in)
}
func TestSelectNotification(t *testing.T) {
notifier, err := SelectNotification(EXAMPLE_ID)
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "example", notifier.Method)
assert.False(t, notifier.Enabled)
}
func TestAddQueue(t *testing.T) {
msg := "this is a test in the queue!"
example.AddQueue(msg)
assert.Equal(t, 1, len(example.Queue))
example.AddQueue(msg)
assert.Equal(t, 2, len(example.Queue))
example.AddQueue(msg)
assert.Equal(t, 3, len(example.Queue))
example.AddQueue(msg)
assert.Equal(t, 4, len(example.Queue))
example.AddQueue(msg)
assert.Equal(t, 5, len(example.Queue))
}
func TestNotification_Update(t *testing.T) {
notifier, err := SelectNotification(EXAMPLE_ID)
notifier, err := SelectNotification(example)
assert.Nil(t, err)
notifier.Host = "new host here"
notifier.Host = "http://demo.statup.io/api"
notifier.Port = 9090
notifier.Username = "admin"
notifier.Password = "password123"
notifier.Var1 = "var1_is_here"
notifier.Var2 = "var2_is_here"
notifier.ApiKey = "USBdu82HDiiuw9327yGYDGw"
notifier.ApiSecret = "PQopncow929hUIDHGwiud"
notifier.Limits = 15
_, err = notifier.Update()
assert.Nil(t, err)
selected, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "http://demo.statup.io/api", selected.GetValue("host"))
assert.Equal(t, "http://demo.statup.io/api", example.Notification.Host)
assert.Equal(t, "http://demo.statup.io/api", example.Host)
assert.Equal(t, "USBdu82HDiiuw9327yGYDGw", selected.GetValue("api_key"))
assert.Equal(t, "USBdu82HDiiuw9327yGYDGw", example.ApiKey)
assert.False(t, selected.Enabled)
}
func TestEnableNotification(t *testing.T) {
notifier, err := SelectNotification(example)
notifier.Enabled = true
updated, err := notifier.Update()
assert.Nil(t, err)
selected, err := SelectNotification(updated.Method)
assert.True(t, updated.Enabled)
}
func TestIsEnabled(t *testing.T) {
assert.True(t, isEnabled(example))
}
func TestLastSent(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, "new host here", selected.GetValue("host"))
assert.True(t, selected.Enabled)
assert.Equal(t, "0s", notifier.LastSent().String())
}
func TestWithinLimits(t *testing.T) {
notifier, err := SelectNotification(example)
assert.Nil(t, err)
assert.Equal(t, 15, notifier.Limit())
assert.Equal(t, 15, notifier.Limits)
assert.True(t, inLimits(example))
}
func TestNotification_GetValue(t *testing.T) {
notifier, err := SelectNotification(EXAMPLE_ID)
notifier, err := SelectNotification(example)
assert.Nil(t, err)
val := notifier.GetValue("Host")
assert.Equal(t, "http://exmaplehost.com", val)
assert.Equal(t, "http://demo.statup.io/api", val)
}
//func TestRun(t *testing.T) {
// err := example.Run()
// assert.Equal(t, "running", err.Error())
//}
func TestRunQue(t *testing.T) {
assert.Nil(t, RunQue(example))
assert.Equal(t, 4, len(example.Queue))
assert.Nil(t, RunQue(example))
assert.Equal(t, 3, len(example.Queue))
assert.Nil(t, RunQue(example))
assert.Equal(t, 2, len(example.Queue))
//func TestTestIt(t *testing.T) {
// err := example.Test()
// assert.Equal(t, "testing", err.Error())
//}
time.Sleep(2 * time.Second)
assert.True(t, example.LastSent().Seconds() >= float64(2))
assert.Nil(t, RunQue(example))
assert.Equal(t, 1, len(example.Queue))
assert.Nil(t, RunQue(example))
assert.Equal(t, 0, len(example.Queue))
assert.Nil(t, RunQue(example))
assert.Equal(t, 0, len(example.Queue))
}
func TestOnSave(t *testing.T) {
err := example.OnSave()
assert.Equal(t, "onsave triggered", err.Error())
}
func TestOnSuccess(t *testing.T) {
s := &types.Service{
Name: "Interpol - All The Rage Back Home",
Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
}
OnSuccess(s)
OnSuccess(service)
assert.Equal(t, 2, len(example.Queue))
}
func TestOnFailure(t *testing.T) {
s := &types.Service{
Name: "Interpol - All The Rage Back Home",
Domain: "https://www.youtube.com/watch?v=-u6DvRyyKGU",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
}
f := &types.Failure{
Issue: "testing",
}
OnFailure(s, f)
OnFailure(service, failure)
assert.Equal(t, 3, len(example.Queue))
}
func TestOnNewService(t *testing.T) {
OnNewService(service)
assert.Equal(t, 4, len(example.Queue))
}
func TestOnUpdatedService(t *testing.T) {
OnUpdatedService(service)
assert.Equal(t, 5, len(example.Queue))
}
func TestOnDeletedService(t *testing.T) {
OnDeletedService(service)
assert.Equal(t, 6, len(example.Queue))
}
func TestOnNewUser(t *testing.T) {
OnNewUser(user)
assert.Equal(t, 7, len(example.Queue))
}
func TestOnUpdatedUser(t *testing.T) {
OnUpdatedUser(user)
assert.Equal(t, 8, len(example.Queue))
}
func TestOnDeletedUser(t *testing.T) {
OnDeletedUser(user)
assert.Equal(t, 9, len(example.Queue))
}
func TestOnUpdatedCore(t *testing.T) {
OnUpdatedCore(core)
assert.Equal(t, 10, len(example.Queue))
}
func TestOnUpdatedNotifier(t *testing.T) {
OnUpdatedNotifier(example.Select())
assert.Equal(t, 11, len(example.Queue))
}
func TestRunAllQueue(t *testing.T) {
//runQue(example)
}

301
core/sample.go Normal file
View File

@ -0,0 +1,301 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"math/rand"
"time"
)
// InsertSampleData will create the example/dummy services for a brand new Statup installation
func InsertSampleData() error {
utils.Log(1, "Inserting Sample Data...")
s1 := ReturnService(&types.Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 1,
})
s2 := ReturnService(&types.Service{
Name: "Statup Github",
Domain: "https://github.com/hunterlong/statup",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 2,
})
s3 := ReturnService(&types.Service{
Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 30,
Order: 3,
})
s4 := ReturnService(&types.Service{
Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201,
Expected: `(title)": "((\\"|[statup])*)"`,
Interval: 30,
Type: "http",
Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
Timeout: 30,
Order: 4,
})
s5 := ReturnService(&types.Service{
Name: "Google DNS",
Domain: "8.8.8.8",
Interval: 20,
Type: "tcp",
Port: 53,
Timeout: 120,
Order: 5,
})
s1.Create(false)
s2.Create(false)
s3.Create(false)
s4.Create(false)
s5.Create(false)
utils.Log(1, "Sample data has finished importing")
return nil
}
func InsertSampleCore() error {
core := &types.Core{
Name: "Statup Sample Data",
Description: "This data is only used to testing",
ApiKey: "sample",
ApiSecret: "samplesecret",
Domain: "http://localhost:8080",
Version: "test",
CreatedAt: time.Now(),
UseCdn: false,
}
query := coreDB().Create(core)
return query.Error
}
func insertSampleUsers() {
u2 := ReturnUser(&types.User{
Username: "testadmin",
Password: "password123",
Email: "info@betatude.com",
Admin: true,
})
u3 := ReturnUser(&types.User{
Username: "testadmin2",
Password: "password123",
Email: "info@adminhere.com",
Admin: true,
})
u2.Create()
u3.Create()
}
// InsertSampleData will create the example/dummy services for a brand new Statup installation
func InsertLargeSampleData() error {
InsertSampleCore()
InsertSampleData()
insertSampleUsers()
s6 := ReturnService(&types.Service{
Name: "JSON Lint",
Domain: "https://jsonlint.com",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 6,
})
s7 := ReturnService(&types.Service{
Name: "Demo Page",
Domain: "https://demo.statup.io",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 15,
Order: 7,
})
s8 := ReturnService(&types.Service{
Name: "Golang",
Domain: "https://golang.org",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 8,
})
s9 := ReturnService(&types.Service{
Name: "Santa Monica",
Domain: "https://www.santamonica.com",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 9,
})
s10 := ReturnService(&types.Service{
Name: "Oeschs Die Dritten",
Domain: "https://www.oeschs-die-dritten.ch/en/",
ExpectedStatus: 200,
Interval: 15,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 10,
})
s11 := ReturnService(&types.Service{
Name: "XS Project - Bochka, Bass, Kolbaser",
Domain: "https://www.youtube.com/watch?v=VLW1ieY4Izw",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 11,
})
s12 := ReturnService(&types.Service{
Name: "Github",
Domain: "https://github.com/hunterlong",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 20,
Order: 12,
})
s13 := ReturnService(&types.Service{
Name: "Failing URL",
Domain: "http://thisdomainisfakeanditsgoingtofail.com",
ExpectedStatus: 200,
Interval: 45,
Type: "http",
Method: "GET",
Timeout: 10,
Order: 13,
})
s14 := ReturnService(&types.Service{
Name: "Oesch's die Dritten - Die Jodelsprache",
Domain: "https://www.youtube.com/watch?v=k3GTxRt4iao",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 12,
Order: 14,
})
s15 := ReturnService(&types.Service{
Name: "Gorm",
Domain: "http://gorm.io/",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 12,
Order: 15,
})
s6.Create(false)
s7.Create(false)
s8.Create(false)
s9.Create(false)
s10.Create(false)
s11.Create(false)
s12.Create(false)
s13.Create(false)
s14.Create(false)
s15.Create(false)
var dayAgo = time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
insertHitRecords(dayAgo, 1450)
insertFailureRecords(dayAgo, 730)
return nil
}
func insertFailureRecords(since time.Time, amount int64) {
for i := int64(14); i <= 15; i++ {
service := SelectService(i)
utils.Log(1, fmt.Sprintf("Adding %v failure records to service %v", amount, service.Name))
createdAt := since
for fi := int64(1); fi <= amount; fi++ {
createdAt = createdAt.Add(2 * time.Minute)
failure := &types.Failure{
Service: service.Id,
Issue: "testing right here",
CreatedAt: createdAt,
}
service.CreateFailure(failure)
}
}
}
func insertHitRecords(since time.Time, amount int64) {
for i := int64(1); i <= 15; i++ {
service := SelectService(i)
utils.Log(1, fmt.Sprintf("Adding %v hit records to service %v", amount, service.Name))
createdAt := since
for hi := int64(1); hi <= amount; hi++ {
rand.Seed(time.Now().UnixNano())
latency := rand.Float64()
createdAt = createdAt.Add(1 * time.Minute)
hit := &types.Hit{
Service: service.Id,
CreatedAt: createdAt,
Latency: latency,
}
service.CreateHit(hit)
}
}
}

View File

@ -56,6 +56,7 @@ func (c *Core) SelectAllServices() ([]*Service, error) {
utils.Log(3, fmt.Sprintf("service error: %v", db.Error))
return nil, db.Error
}
CoreApp.Services = nil
for _, service := range services {
service.Start()
service.AllCheckins()
@ -164,15 +165,17 @@ func GroupDataBy(column string, id int64, tm time.Time, increment string) string
return sql
}
// GraphData returns the JSON object used by Charts.js to render the chart
func (s *Service) GraphData() string {
type graphObject struct {
}
func (s *Service) GraphDataRaw() []*DateScan {
var d []*DateScan
since := time.Now().Add(time.Hour*-24 + time.Minute*0 + time.Second*0)
sql := GroupDataBy("hits", s.Id, since, "minute")
rows, err := DbSession.Raw(sql).Rows()
if err != nil {
utils.Log(2, err)
return ""
return nil
}
for rows.Next() {
gd := new(DateScan)
@ -189,7 +192,13 @@ func (s *Service) GraphData() string {
gd.Value = int64(ff)
d = append(d, gd)
}
data, err := json.Marshal(d)
return d
}
// GraphData returns the JSON object used by Charts.js to render the chart
func (s *Service) GraphData() string {
obj := s.GraphDataRaw()
data, err := json.Marshal(obj)
if err != nil {
utils.Log(2, err)
return ""
@ -298,7 +307,7 @@ func (u *Service) Update(restart bool) error {
}
// Create will create a service and insert it into the database
func (u *Service) Create() (int64, error) {
func (u *Service) Create(check bool) (int64, error) {
u.CreatedAt = time.Now()
db := servicesDB().Create(u)
if db.Error != nil {
@ -306,7 +315,7 @@ func (u *Service) Create() (int64, error) {
return 0, db.Error
}
u.Start()
go u.CheckQueue(true)
go u.CheckQueue(check)
CoreApp.Services = append(CoreApp.Services, u)
reorderServices()
notifier.OnNewService(u.Service)

View File

@ -29,7 +29,7 @@ var (
func TestSelectHTTPService(t *testing.T) {
services, err := CoreApp.SelectAllServices()
assert.Nil(t, err)
assert.Equal(t, 18, len(services))
assert.Equal(t, 15, len(services))
assert.Equal(t, "Google", services[0].Name)
assert.Equal(t, "http", services[0].Type)
}
@ -42,12 +42,12 @@ func TestSelectAllServices(t *testing.T) {
assert.True(t, service.IsRunning())
t.Logf("ID: %v %v\n", service.Id, service.Name)
}
assert.Equal(t, 18, len(services))
assert.Equal(t, 15, len(services))
}
func TestSelectTCPService(t *testing.T) {
services := CoreApp.Services
assert.Equal(t, 18, len(services))
assert.Equal(t, 15, len(services))
service := SelectService(5)
assert.NotNil(t, service)
assert.Equal(t, "Google DNS", service.Name)
@ -111,14 +111,13 @@ func TestCheckTCPService(t *testing.T) {
}
func TestServiceOnline24Hours(t *testing.T) {
since, err := time.Parse(time.RFC3339, SERVICE_SINCE)
assert.Nil(t, err)
since := time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)
assert.True(t, service.OnlineSince(since) > 80)
assert.Equal(t, float32(100), service.OnlineSince(since))
service2 := SelectService(5)
assert.Equal(t, float32(100), service2.OnlineSince(since))
service3 := SelectService(18)
assert.Equal(t, float32(0), service3.OnlineSince(since))
service3 := SelectService(14)
assert.Equal(t, float32(49.69), service3.OnlineSince(since))
}
func TestServiceSmallText(t *testing.T) {
@ -128,35 +127,36 @@ func TestServiceSmallText(t *testing.T) {
}
func TestServiceAvgUptime(t *testing.T) {
since, err := time.Parse(time.RFC3339, SERVICE_SINCE)
assert.Nil(t, err)
since := time.Now().Add(-24 * time.Hour).Add(-10 * time.Minute)
service := SelectService(1)
assert.NotEqual(t, "0.00", service.AvgUptime(since))
service2 := SelectService(5)
assert.Equal(t, "100", service2.AvgUptime(since))
service3 := SelectService(18)
assert.Equal(t, "0.00", service3.AvgUptime(since))
service3 := SelectService(13)
assert.Equal(t, "100", service3.AvgUptime(since))
service4 := SelectService(15)
assert.Equal(t, "49.69", service4.AvgUptime(since))
}
func TestServiceHits(t *testing.T) {
service := SelectService(5)
hits, err := service.Hits()
assert.Nil(t, err)
assert.Equal(t, int(5), len(hits))
assert.Equal(t, int(1452), len(hits))
}
func TestServiceLimitedHits(t *testing.T) {
service := SelectService(5)
hits, err := service.LimitedHits()
assert.Nil(t, err)
assert.Equal(t, int(5), len(hits))
assert.Equal(t, int(1024), len(hits))
}
func TestServiceTotalHits(t *testing.T) {
service := SelectService(5)
hits, err := service.TotalHits()
assert.Nil(t, err)
assert.Equal(t, uint64(0x5), hits)
assert.Equal(t, uint64(0x5ac), hits)
}
func TestServiceSum(t *testing.T) {
@ -168,7 +168,7 @@ func TestServiceSum(t *testing.T) {
func TestCountOnline(t *testing.T) {
amount := CoreApp.CountOnline()
assert.Equal(t, 4, amount)
assert.True(t, amount >= 2)
}
func TestCreateService(t *testing.T) {
@ -182,7 +182,7 @@ func TestCreateService(t *testing.T) {
Timeout: 20,
})
var err error
newServiceId, err = s.Create()
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
@ -205,7 +205,7 @@ func TestCreateFailingHTTPService(t *testing.T) {
Timeout: 5,
})
var err error
newServiceId, err = s.Create()
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
@ -214,7 +214,7 @@ func TestCreateFailingHTTPService(t *testing.T) {
}
func TestServiceFailedCheck(t *testing.T) {
service := SelectService(20)
service := SelectService(17)
assert.Equal(t, "Bad URL", service.Name)
service.Check(true)
assert.Equal(t, "Bad URL", service.Name)
@ -231,7 +231,7 @@ func TestCreateFailingTCPService(t *testing.T) {
Timeout: 5,
})
var err error
newServiceId, err = s.Create()
newServiceId, err = s.Create(false)
assert.Nil(t, err)
assert.NotZero(t, newServiceId)
newService := SelectService(newServiceId)
@ -240,7 +240,7 @@ func TestCreateFailingTCPService(t *testing.T) {
}
func TestServiceFailedTCPCheck(t *testing.T) {
service := SelectService(21)
service := SelectService(newServiceId)
service.Check(true)
assert.Equal(t, "Bad TCP", service.Name)
assert.False(t, service.Online)
@ -262,13 +262,13 @@ func TestDeleteService(t *testing.T) {
count, err := CoreApp.SelectAllServices()
assert.Nil(t, err)
assert.Equal(t, 21, len(count))
assert.Equal(t, 18, len(count))
err = service.Delete()
assert.Nil(t, err)
services := CoreApp.Services
assert.Equal(t, 59, len(services))
assert.Equal(t, 17, len(services))
}
func TestServiceCloseRoutine(t *testing.T) {

View File

@ -1,127 +0,0 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package core
import (
"fmt"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"os"
)
// DeleteConfig will delete the 'config.yml' file
func DeleteConfig() {
err := os.Remove(utils.Directory + "/config.yml")
if err != nil {
utils.Log(3, err)
}
}
type ErrorResponse struct {
Error string
}
// LoadSampleData will create the example/dummy services for a brand new Statup installation
func LoadSampleData() error {
utils.Log(1, "Inserting Sample Data...")
s1 := ReturnService(&types.Service{
Name: "Google",
Domain: "https://google.com",
ExpectedStatus: 200,
Interval: 10,
Type: "http",
Method: "GET",
Timeout: 10,
})
s2 := ReturnService(&types.Service{
Name: "Statup Github",
Domain: "https://github.com/hunterlong/statup",
ExpectedStatus: 200,
Interval: 30,
Type: "http",
Method: "GET",
Timeout: 20,
})
s3 := ReturnService(&types.Service{
Name: "JSON Users Test",
Domain: "https://jsonplaceholder.typicode.com/users",
ExpectedStatus: 200,
Interval: 60,
Type: "http",
Method: "GET",
Timeout: 30,
})
s4 := ReturnService(&types.Service{
Name: "JSON API Tester",
Domain: "https://jsonplaceholder.typicode.com/posts",
ExpectedStatus: 201,
Expected: `(title)": "((\\"|[statup])*)"`,
Interval: 30,
Type: "http",
Method: "POST",
PostData: `{ "title": "statup", "body": "bar", "userId": 19999 }`,
Timeout: 30,
})
s5 := ReturnService(&types.Service{
Name: "Google DNS",
Domain: "8.8.8.8",
Interval: 20,
Type: "tcp",
Port: 53,
Timeout: 120,
})
id, err := s1.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
}
id, err = s2.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
}
id, err = s3.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
}
id, err = s4.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating Service %v: %v", id, err))
}
id, err = s5.Create()
if err != nil {
utils.Log(3, fmt.Sprintf("Error creating TCP Service %v: %v", id, err))
}
//checkin := &Checkin{
// Service: s2.Id,
// Interval: 30,
// Api: utils.NewSHA1Hash(18),
//}
//id, err = checkin.Create()
//if err != nil {
// utils.Log(3, fmt.Sprintf("Error creating Checkin %v: %v", id, err))
//}
//for i := 0; i < 3; i++ {
// s1.Check()
// s2.Check()
// s3.Check()
// s4.Check()
//}
utils.Log(1, "Sample data has finished importing")
return nil
}

View File

@ -36,13 +36,13 @@ func TestCreateUser(t *testing.T) {
func TestSelectAllUsers(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
assert.Equal(t, 2, len(users))
assert.Equal(t, 3, len(users))
}
func TestSelectUser(t *testing.T) {
user, err := SelectUser(1)
assert.Nil(t, err)
assert.Equal(t, "info@statup.io", user.Email)
assert.Equal(t, "info@betatude.com", user.Email)
assert.True(t, user.Admin)
}
@ -50,7 +50,7 @@ func TestSelectUsername(t *testing.T) {
user, err := SelectUsername("hunter")
assert.Nil(t, err)
assert.Equal(t, "test@email.com", user.Email)
assert.Equal(t, int64(2), user.Id)
assert.Equal(t, int64(3), user.Id)
assert.True(t, user.Admin)
}
@ -80,7 +80,7 @@ func TestCreateUser2(t *testing.T) {
func TestSelectAllUsersAgain(t *testing.T) {
users, err := SelectAllUsers()
assert.Nil(t, err)
assert.Equal(t, 3, len(users))
assert.Equal(t, 4, len(users))
}
func TestAuthUser(t *testing.T) {
@ -88,7 +88,7 @@ func TestAuthUser(t *testing.T) {
assert.True(t, auth)
assert.NotNil(t, user)
assert.Equal(t, "user@email.com", user.Email)
assert.Equal(t, int64(3), user.Id)
assert.Equal(t, int64(4), user.Id)
assert.True(t, user.Admin)
}

View File

@ -1,127 +0,0 @@
// +build test
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package example
import (
"fmt"
"github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/types"
"sync"
)
var (
exampler *Example
slackMessages []string
messageLock *sync.Mutex
)
type Example struct {
*notifiers.Notification
}
// DEFINE YOUR NOTIFICATION HERE.
func init() {
exampler = &Example{&notifiers.Notification{
Id: 99999,
Method: "slack",
Host: "https://webhooksurl.slack.com/***",
Form: []notifiers.NotificationForm{{
Type: "text",
Title: "Incoming Webhook Url",
Placeholder: "Insert your Slack webhook URL here.",
DbField: "Host",
}}},
}
notifiers.AddNotifier(exampler)
messageLock = new(sync.Mutex)
}
// Select Obj
func (u *Example) Select() *notifiers.Notification {
return u.Notification
}
// WHEN NOTIFIER LOADS
func (u *Example) Init() error {
err := u.Install()
if err == nil {
notifier, _ := notifiers.SelectNotification(u.Id)
forms := u.Form
u.Notification = notifier
u.Form = forms
if u.Enabled {
go u.Run()
}
}
return err
}
func (u *Example) Test() error {
fmt.Println("Example notifier has been Tested!")
return nil
}
// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS
func (u *Example) Run() error {
if u.Enabled {
u.Run()
}
return nil
}
// CUSTOM FUNCTION FO SENDING SLACK MESSAGES
func SendSlack(temp string, data interface{}) error {
return nil
}
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Example) OnFailure(s *types.Service) error {
if u.Enabled {
fmt.Println("Example notifier received a failing service event!")
}
return nil
}
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS
func (u *Example) OnSuccess(s *types.Service) error {
if u.Enabled {
fmt.Println("Example notifier received a successful service event!")
}
return nil
}
// ON SAVE OR UPDATE OF THE NOTIFIER FORM
func (u *Example) OnSave() error {
fmt.Println("Example notifier was saved!")
return nil
}
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Example) Install() error {
inDb := exampler.Notification.IsInDatabase()
if !inDb {
newNotifer, err := notifiers.InsertDatabase(u.Notification)
if err != nil {
return err
}
fmt.Println("Example notifier was installed!", newNotifer)
}
return nil
}

View File

@ -93,6 +93,22 @@ func ApiServiceHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(service)
}
func ApiServiceDataHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
return
}
vars := mux.Vars(r)
service := core.SelectService(utils.StringInt(vars["id"]))
if service == nil {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(service.GraphDataRaw())
}
func ApiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
if !isAPIAuthorized(r) {
http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
@ -106,7 +122,7 @@ func ApiCreateServiceHandler(w http.ResponseWriter, r *http.Request) {
return
}
newService := core.ReturnService(service)
_, err = newService.Create()
_, err = newService.Create(true)
if err != nil {
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
return

View File

@ -61,14 +61,12 @@ func loadDatabase() {
func createDatabase() {
core.Configs.DropDatabase()
core.Configs.CreateDatabase()
core.InitApp()
}
func resetDatabase() {
core.Configs.DropDatabase()
core.Configs.CreateDatabase()
core.Configs.SeedDatabase()
core.InitApp()
core.InsertLargeSampleData()
}
func Clean() {
@ -91,7 +89,6 @@ func formatJSON(res string, out interface{}) {
}
func TestApiIndexHandler(t *testing.T) {
rr, err := httpRequestAPI(t, "GET", "/api", nil)
assert.Nil(t, err)
body := rr.Body.String()
@ -99,12 +96,11 @@ func TestApiIndexHandler(t *testing.T) {
var obj types.Core
formatJSON(body, &obj)
assert.Equal(t, 200, rr.Code)
assert.Equal(t, "Awesome Status", obj.Name)
assert.Equal(t, "Statup Sample Data", obj.Name)
assert.Equal(t, "sqlite", obj.DbConnection)
}
func TestApiAllServicesHandlerHandler(t *testing.T) {
rr, err := httpRequestAPI(t, "GET", "/api/services", nil)
assert.Nil(t, err)
body := rr.Body.String()
@ -117,11 +113,9 @@ func TestApiAllServicesHandlerHandler(t *testing.T) {
}
func TestApiServiceHandler(t *testing.T) {
rr, err := httpRequestAPI(t, "GET", "/api/services/1", nil)
assert.Nil(t, err)
body := rr.Body.String()
t.Log(body)
var obj types.Service
formatJSON(body, &obj)
assert.Equal(t, 200, rr.Code)
@ -129,8 +123,17 @@ func TestApiServiceHandler(t *testing.T) {
assert.Equal(t, "https://google.com", obj.Domain)
}
func TestApiCreateServiceHandler(t *testing.T) {
func TestApiServiceDataHandler(t *testing.T) {
rr, err := httpRequestAPI(t, "GET", "/api/services/1/data", nil)
assert.Nil(t, err)
body := rr.Body.String()
var obj []*core.DateScan
formatJSON(body, &obj)
assert.Equal(t, 200, rr.Code)
assert.Equal(t, 60, len(obj))
}
func TestApiCreateServiceHandler(t *testing.T) {
rr, err := httpRequestAPI(t, "POST", "/api/services", strings.NewReader(NEW_HTTP_SERVICE))
assert.Nil(t, err)
body := rr.Body.String()
@ -189,7 +192,7 @@ func TestApiAllUsersHandler(t *testing.T) {
var obj []types.User
formatJSON(body, &obj)
assert.Equal(t, true, obj[0].Admin)
assert.Equal(t, "admin", obj[0].Username)
assert.Equal(t, "testadmin", obj[0].Username)
}
func TestApiCreateUserHandler(t *testing.T) {
@ -215,7 +218,7 @@ func TestApiViewUserHandler(t *testing.T) {
assert.Equal(t, 200, rr.Code)
var obj types.User
formatJSON(body, &obj)
assert.Equal(t, "admin2", obj.Username)
assert.Equal(t, "testadmin2", obj.Username)
assert.Equal(t, true, obj.Admin)
}

View File

@ -18,6 +18,7 @@ package handlers
import (
"fmt"
"github.com/hunterlong/statup/core"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/utils"
"net/http"
)
@ -64,7 +65,8 @@ func HelpHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
ExecuteResponse(w, r, "help.html", nil, nil)
help := source.HelpMarkdown()
ExecuteResponse(w, r, "help.html", help, nil)
}
func LogsHandler(w http.ResponseWriter, r *http.Request) {

View File

@ -18,6 +18,7 @@ package handlers
import (
"fmt"
"github.com/hunterlong/statup/core"
_ "github.com/hunterlong/statup/notifiers"
"github.com/hunterlong/statup/source"
"github.com/hunterlong/statup/utils"
"github.com/stretchr/testify/assert"
@ -494,12 +495,11 @@ func TestPrometheusHandler(t *testing.T) {
Router().ServeHTTP(rr, req)
body := rr.Body.String()
assert.Equal(t, 200, rr.Code)
assert.Contains(t, body, "statup_total_services 11")
assert.Contains(t, body, "statup_total_services 6")
assert.True(t, isRouteAuthenticated(req))
}
func TestSaveNotificationHandler(t *testing.T) {
t.SkipNow()
form := url.Values{}
form.Add("enable", "on")
form.Add("host", "smtp.emailer.com")
@ -511,17 +511,16 @@ func TestSaveNotificationHandler(t *testing.T) {
form.Add("api_key", "")
form.Add("api_secret", "")
form.Add("limits", "7")
req, err := http.NewRequest("POST", "/settings/notifier/1", strings.NewReader(form.Encode()))
req, err := http.NewRequest("POST", "/settings/notifier/email", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
assert.Nil(t, err)
rr := httptest.NewRecorder()
Router().ServeHTTP(rr, req)
assert.Equal(t, 200, rr.Code)
assert.Equal(t, 303, rr.Code)
assert.True(t, isRouteAuthenticated(req))
}
func TestViewNotificationSettingsHandler(t *testing.T) {
t.SkipNow()
req, err := http.NewRequest("GET", "/settings", nil)
assert.Nil(t, err)
rr := httptest.NewRecorder()

View File

@ -105,7 +105,7 @@ func DesktopInit(ip string, port int) {
})
admin.Create()
core.LoadSampleData()
core.InsertSampleData()
config.ApiKey = core.CoreApp.ApiKey
config.ApiSecret = core.CoreApp.ApiSecret

View File

@ -84,6 +84,7 @@ func Router() *mux.Router {
r.Handle("/api/services", http.HandlerFunc(ApiAllServicesHandler)).Methods("GET")
r.Handle("/api/services", http.HandlerFunc(ApiCreateServiceHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceHandler)).Methods("GET")
r.Handle("/api/services/{id}/data", http.HandlerFunc(ApiServiceDataHandler)).Methods("GET")
r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceUpdateHandler)).Methods("POST")
r.Handle("/api/services/{id}", http.HandlerFunc(ApiServiceDeleteHandler)).Methods("DELETE")

View File

@ -114,7 +114,7 @@ func CreateServiceHandler(w http.ResponseWriter, r *http.Request) {
Timeout: timeout,
Order: order,
})
_, err := service.Create()
_, err := service.Create(true)
if err != nil {
utils.Log(3, fmt.Sprintf("Error starting %v check routine. %v", service.Name, err))
}

View File

@ -130,12 +130,9 @@ func SaveNotificationHandler(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/", http.StatusSeeOther)
return
}
form := parseForm(r)
vars := mux.Vars(r)
method := vars["method"]
enabled := form.Get("enable")
host := form.Get("host")
port := int(utils.StringInt(form.Get("port")))

View File

@ -139,7 +139,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) {
admin.Create()
if sample == "on" {
core.LoadSampleData()
core.InsertSampleData()
}
core.InitApp()

View File

@ -20,8 +20,8 @@ import (
"fmt"
"github.com/hunterlong/statup/core/notifier"
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"net/http"
"time"
)
const (
@ -34,8 +34,13 @@ type Discord struct {
}
var discorder = &Discord{&notifier.Notification{
Method: DISCORD_METHOD,
Host: "https://discordapp.com/api/webhooks/****/*****",
Method: DISCORD_METHOD,
Title: "Discord",
Description: "Send notifications to your discord channel using discord webhooks. Insert your Discord channel webhook URL to receive notifications. Based on the <a href=\"https://discordapp.com/developers/docs/resources/webhook\">Discord Webhook API</a>.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(5 * time.Second),
Host: "https://discordapp.com/api/webhooks/****/*****",
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Discord Webhook URL",
@ -52,39 +57,32 @@ func init() {
}
}
func (u *Discord) Test() error {
utils.Log(1, "Discord notifier loaded")
discordPost([]byte(DISCORD_TEST))
return nil
}
// Discord won't be using the Run() process
func (u *Discord) Run() error {
return nil
}
// Discord won't be using the Run() process
func (u *Discord) Select() *notifier.Notification {
return u.Notification
}
// discordPost sends an HTTP POST to the webhook URL
func discordPost(msg []byte) {
req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer(msg))
func (u *Discord) Send(msg interface{}) error {
message := msg.([]byte)
fmt.Println("sending: ", message)
req, _ := http.NewRequest("POST", discorder.GetValue("host"), bytes.NewBuffer(message))
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
utils.Log(3, fmt.Sprintf("issue sending Discord message to channel: %v", err))
return
return err
}
defer resp.Body.Close()
discorder.Log(string(msg))
return resp.Body.Close()
}
func (u *Discord) Test() error {
u.AddQueue([]byte(DISCORD_TEST))
return nil
}
func (u *Discord) Select() *notifier.Notification {
return u.Notification
}
func (u *Discord) OnFailure(s *types.Service, f *types.Failure) {
msg := fmt.Sprintf(`{"content": "Your service '%v' is currently failing! Reason: %v"}`, s.Name, f.Issue)
discordPost([]byte(msg))
fmt.Println(msg)
u.AddQueue(msg)
}
func (u *Discord) OnSuccess(s *types.Service) {
@ -93,6 +91,6 @@ func (u *Discord) OnSuccess(s *types.Service) {
func (u *Discord) OnSave() error {
msg := fmt.Sprintf(`{"content": "The Discord notifier on Statup was just updated."}`)
discordPost([]byte(msg))
u.AddQueue(msg)
return nil
}

File diff suppressed because one or more lines are too long

View File

@ -23,24 +23,22 @@ import (
"net/http"
"net/url"
"strings"
"time"
)
const (
LINE_NOTIFY_ID = 4
LINE_NOTIFY_METHOD = "line notify"
)
var (
lineNotifyMessages []string
)
type LineNotify struct {
*notifier.Notification
}
var lineNotify = &LineNotify{&notifier.Notification{
Method: LINE_NOTIFY_METHOD,
Method: LINE_NOTIFY_METHOD,
Title: "LINE Notify",
Description: "LINE Notify will send notifications to your LINE Notify account when services are offline or online. Baed on the <a href=\"https://notify-bot.line.me/doc/en/\">LINE Notify API</a>.",
Author: "Kanin Peanviriyakulkit",
AuthorUrl: "https://github.com/dogrocker",
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Access Token",
@ -53,66 +51,40 @@ var lineNotify = &LineNotify{&notifier.Notification{
func init() {
err := notifier.AddNotifier(lineNotify)
if err != nil {
utils.Log(3, err)
panic(err)
}
}
func (u *LineNotify) postUrl() string {
return fmt.Sprintf("https://notify-api.line.me/api/notify")
func (u *LineNotify) Send(msg interface{}) error {
message := msg.(string)
client := new(http.Client)
v := url.Values{}
v.Set("message", message)
req, err := http.NewRequest("POST", "https://notify-api.line.me/api/notify", strings.NewReader(v.Encode()))
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", u.GetValue("api_secret")))
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
_, err = client.Do(req)
if err != nil {
return err
}
return nil
}
func (u *LineNotify) Select() *notifier.Notification {
return u.Notification
}
func (u *LineNotify) Test() error {
msg := fmt.Sprintf("You're Statup Line Notify Notifier is working correctly!")
SendLineNotify(msg)
return nil
}
// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS
func (u *LineNotify) Run() error {
lineNotifyMessages = notifier.UniqueStrings(lineNotifyMessages)
for _, msg := range lineNotifyMessages {
if u.CanSend() {
utils.Log(1, fmt.Sprintf("Sending Line Notify Message"))
lineNotifyUrl := u.postUrl()
client := &http.Client{}
v := url.Values{}
v.Set("message", msg)
rb := *strings.NewReader(v.Encode())
req, err := http.NewRequest("POST", lineNotifyUrl, &rb)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %v", u.ApiSecret))
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client.Do(req)
if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Line Notify notification: %v", err))
}
u.Log(msg)
}
}
lineNotifyMessages = []string{}
time.Sleep(60 * time.Second)
if u.Enabled {
u.Run()
}
return nil
}
// CUSTOM FUNCTION FO SENDING LINE NOTIFY MESSAGES
func SendLineNotify(data string) error {
lineNotifyMessages = append(lineNotifyMessages, data)
u.AddQueue(msg)
return nil
}
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *LineNotify) OnFailure(s *types.Service, f *types.Failure) {
if u.Enabled {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
SendLineNotify(msg)
}
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(msg)
}
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS

File diff suppressed because one or more lines are too long

View File

@ -22,7 +22,6 @@ import (
"github.com/hunterlong/statup/types"
"github.com/hunterlong/statup/utils"
"net/http"
"sync"
"text/template"
"time"
)
@ -35,93 +34,80 @@ const (
TEST_TEMPLATE = `{"text":"{{.}}"}`
)
var (
slackMessages []string
messageLock *sync.Mutex
)
type Slack struct {
*notifier.Notification
}
type slackMessage struct {
Service *types.Service
Time int64
}
var slacker = &Slack{&notifier.Notification{
Method: SLACK_METHOD,
Host: "https://webhooksurl.slack.com/***",
Method: SLACK_METHOD,
Title: "Slack",
Description: "Send notifications to your Slack channel when a service is offline. Insert your Incoming Webhook URL for your channel to receive notifications. Based on the <a href=\"https://api.slack.com/incoming-webhooks\">Slack API</a>.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(10 * time.Second),
Host: "https://webhooksurl.slack.com/***",
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Incoming Webhook Url",
Placeholder: "Insert your Slack webhook URL here.",
SmallText: "Incoming Webhook URL from <a href=\"https://api.slack.com/apps\" target=\"_blank\">Slack Apps</a>",
DbField: "Host",
}}},
}
func sendSlack(temp string, data interface{}) error {
buf := new(bytes.Buffer)
slackTemp, _ := template.New("slack").Parse(temp)
err := slackTemp.Execute(buf, data)
if err != nil {
return err
}
slacker.AddQueue(buf.String())
return nil
}
type slackMessage struct {
Service *types.Service
Template string
Time int64
}
// DEFINE YOUR NOTIFICATION HERE.
func init() {
err := notifier.AddNotifier(slacker)
messageLock = new(sync.Mutex)
if err != nil {
panic(err)
}
}
func (u *Slack) Send(msg interface{}) error {
message := msg.(string)
client := new(http.Client)
_, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(message)))
if err != nil {
return err
}
return nil
}
func (u *Slack) Select() *notifier.Notification {
return u.Notification
}
func (u *Slack) Test() error {
utils.Log(1, "Slack notifier loaded")
msg := fmt.Sprintf("You're Statup Slack Notifier is working correctly!")
SendSlack(TEST_TEMPLATE, msg)
return nil
}
// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS
func (u *Slack) Run() error {
messageLock.Lock()
slackMessages = notifier.UniqueStrings(slackMessages)
for _, msg := range slackMessages {
if u.CanSend() {
utils.Log(1, fmt.Sprintf("Sending JSON to Slack Webhook"))
client := http.Client{Timeout: 15 * time.Second}
_, err := client.Post(u.Host, "application/json", bytes.NewBuffer([]byte(msg)))
if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Slack notification: %v", err))
}
u.Log(msg)
}
}
slackMessages = []string{}
messageLock.Unlock()
time.Sleep(60 * time.Second)
if u.Enabled {
u.Run()
}
return nil
}
// CUSTOM FUNCTION FO SENDING SLACK MESSAGES
func SendSlack(temp string, data interface{}) error {
messageLock.Lock()
buf := new(bytes.Buffer)
slackTemp, _ := template.New("slack").Parse(temp)
slackTemp.Execute(buf, data)
slackMessages = append(slackMessages, buf.String())
messageLock.Unlock()
slacker.Log(buf.String())
return nil
return sendSlack(TEST_TEMPLATE, msg)
}
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Slack) OnFailure(s *types.Service, f *types.Failure) {
if u.Enabled {
message := slackMessage{
Service: s,
Time: time.Now().Unix(),
}
SendSlack(FAILING_TEMPLATE, message)
message := slackMessage{
Service: s,
Template: FAILURE,
Time: time.Now().Unix(),
}
sendSlack(FAILING_TEMPLATE, message)
}
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS
@ -131,8 +117,7 @@ func (u *Slack) OnSuccess(s *types.Service) {
// ON SAVE OR UPDATE OF THE NOTIFIER FORM
func (u *Slack) OnSave() error {
utils.Log(1, fmt.Sprintf("Notification %v is receiving updated information.", u.Method))
// Do updating stuff here
u.Test()
message := fmt.Sprintf("Notification %v is receiving updated information.", u.Method)
u.AddQueue(message)
return nil
}

View File

@ -40,7 +40,12 @@ type Twilio struct {
}
var twilio = &Twilio{&notifier.Notification{
Method: TWILIO_METHOD,
Method: TWILIO_METHOD,
Title: "Twilio",
Description: "Receive SMS text messages directly to your cellphone when a service is offline. You can use a Twilio test account with limits. This notifier uses the <a href=\"https://www.twilio.com/docs/usage/api\">Twilio API</a>.",
Author: "Hunter Long",
AuthorUrl: "https://github.com/hunterlong",
Delay: time.Duration(10 * time.Second),
Form: []notifier.NotificationForm{{
Type: "text",
Title: "Account Sid",
@ -72,65 +77,42 @@ func init() {
}
}
func (u *Twilio) postUrl() string {
return fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", u.ApiKey)
}
func (u *Twilio) Test() error {
utils.Log(1, "Twilio notifier loaded")
msg := fmt.Sprintf("You're Statup Twilio Notifier is working correctly!")
SendTwilio(msg)
u.AddQueue(msg)
return nil
}
// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS
func (u *Twilio) Run() error {
twilioMessages = notifier.UniqueStrings(twilioMessages)
for _, msg := range twilioMessages {
if u.CanSend() {
utils.Log(1, fmt.Sprintf("Sending Twilio Message"))
twilioUrl := u.postUrl()
client := &http.Client{}
v := url.Values{}
v.Set("To", u.Var1)
v.Set("From", u.Var2)
v.Set("Body", msg)
rb := *strings.NewReader(v.Encode())
req, err := http.NewRequest("POST", twilioUrl, &rb)
req.SetBasicAuth(u.ApiKey, u.ApiSecret)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client.Do(req)
if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Twilio notification: %v", err))
}
u.Log(msg)
}
}
twilioMessages = []string{}
time.Sleep(60 * time.Second)
if u.Enabled {
u.Run()
}
return nil
func (u *Twilio) Select() *notifier.Notification {
return u.Notification
}
// CUSTOM FUNCTION FO SENDING TWILIO MESSAGES
func SendTwilio(data string) error {
twilioMessages = append(twilioMessages, data)
func (u *Twilio) Send(msg interface{}) error {
message := msg.(string)
twilioUrl := fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%v/Messages.json", u.GetValue("api_key"))
client := &http.Client{}
v := url.Values{}
v.Set("To", u.Var1)
v.Set("From", u.Var2)
v.Set("Body", message)
rb := *strings.NewReader(v.Encode())
req, err := http.NewRequest("POST", twilioUrl, &rb)
req.SetBasicAuth(u.ApiKey, u.ApiSecret)
req.Header.Add("Accept", "application/json")
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
client.Do(req)
if err != nil {
utils.Log(3, fmt.Sprintf("Issue sending Twilio notification: %v", err))
return err
}
return nil
}
// ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS
func (u *Twilio) OnFailure(s *types.Service, f *types.Failure) {
if u.Enabled {
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
SendTwilio(msg)
}
msg := fmt.Sprintf("Your service '%v' is currently offline!", s.Name)
u.AddQueue(msg)
}
// ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS

View File

@ -3,4 +3,4 @@
if(hxL>=820){hxL=820}else if(70>=hxL){hxL=70}
ctx.fillStyle='#ffa7a2';ctx.fillText(highestNum+"ms",hxH-40,hyH+15);ctx.fillStyle='#45d642';ctx.fillText(lowestnum+"ms",hxL,hyL+10);console.log("done service_id_{{.Id}}")})}},legend:{display:!1},tooltips:{"enabled":!1},scales:{yAxes:[{display:!1,ticks:{fontSize:20,display:!1,beginAtZero:!1},gridLines:{display:!1}}],xAxes:[{type:'time',distribution:'series',autoSkip:!1,gridLines:{display:!1},ticks:{stepSize:1,min:0,fontColor:"white",fontSize:20,display:!1,}}]},elements:{point:{radius:0}}}})
{{ end }}
{{ end }}
{{ end }}

View File

@ -20,6 +20,7 @@ import (
"fmt"
"github.com/GeertJohan/go.rice"
"github.com/hunterlong/statup/utils"
"gopkg.in/russross/blackfriday.v2"
"io"
"io/ioutil"
"os"
@ -41,6 +42,16 @@ func Assets() {
TmplBox = rice.MustFindBox("tmpl")
}
func HelpMarkdown() string {
helpSrc, err := TmplBox.Bytes("help.md")
if err != nil {
utils.Log(4, err)
return "error generating markdown"
}
output := blackfriday.Run(helpSrc)
return string(output)
}
// CompileSASS will attempt to compile the SASS files into CSS
func CompileSASS(folder string) error {
sassBin := os.Getenv("SASS")

View File

@ -21,38 +21,23 @@
{{end}}
<div class="col-12">
<h2>Statup v{{ VERSION }} Help</h2>
Statup is an easy to use Status Page monitor for your websites and applications. Statup is developed in Go Language and you are able to create custom plugins with it!
<p>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/stars/hunterlong/statup.svg?style=social&label=Stars"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/docker/build/hunterlong/statup.svg"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/release/hunterlong/statup.svg"></a>
</p>
<h2 class="mt-3">Services</h2>
For each website and application you want to add a new Service. Each Service will require a URL endpoint to test your applications status.
You can also add expected HTTP responses (regex allow), expected HTTP response codes, and other fields to make sure your service is online or offline.
<h2 class="mt-3">Users</h2>
Users can access the Statup Dashboard to add, remove, and view services.
<h2 class="mt-3">Plugins</h2>
Creating a plugin for Statup is not that difficult, if you know a little bit of Go Language you can create any type of application to be embedded into the Status framework.
Checkout the example plugin that includes all the interfaces, information, and custom HTTP routing at <a href="https://github.com/hunterlong/statup_plugin">https://github.com/hunterlong/statup_plugin</a>.
Anytime there is an action on your status page, all of your plugins will be notified of the change with the values that were changed or created.
<p></p>
Using the statup/plugin Golang package you can quickly implement the event listeners. Statup uses <a href="https://github.com/upper/db">upper.io/db.v3</a> for the database connection.
You can use the database inside of your plugin to create, update, and destroy tables/data. <b>Please only use respectable plugins!</b>
<h2 class="mt-3">Custom Stlying</h2>
On Statup Status Page server can you create your own custom stylesheet to be rendered on the index view of your status page. Go to <a href="/settings">Settings</a> and click on Custom Styling.
{{ safe . }}
</div>
</div>
<style>
pre {
background-color: white;
padding: 10px 15px;
border: 1px solid #a2a2a233;
border-radius: 7px;
}
code {
color: #d87e1a;
}
</style>
{{template "footer"}}
{{if USE_CDN}}
@ -66,4 +51,4 @@
{{end}}
</body>
</html>
</html>

434
source/tmpl/help.md Normal file
View File

@ -0,0 +1,434 @@
# Statup Help
Statup is an easy to use Status Page monitor for your websites and applications. Statup is developed in Go Language and you are able to create custom plugins with it!
<p>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/stars/hunterlong/statup.svg?style=social&label=Stars"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/docker/build/hunterlong/statup.svg"></a>
<a href="https://github.com/hunterlong/statup"><img src="https://img.shields.io/github/release/hunterlong/statup.svg"></a>
</p>
# Services
For each website and application you want to add a new Service. Each Service will require a URL endpoint to test your applications status.
You can also add expected HTTP responses (regex allow), expected HTTP response codes, and other fields to make sure your service is online or offline.
# Statup Settings
You can change multiple settings in your Statup instance.
# Users
Users can access the Statup Dashboard to add, remove, and view services.
# Notifications
# Plugins
Creating a plugin for Statup is not that difficult, if you know a little bit of Go Language you can create any type of application to be embedded into the Status framework.
Checkout the example plugin that includes all the interfaces, information, and custom HTTP routing at <a href="https://github.com/hunterlong/statup_plugin">https://github.com/hunterlong/statup_plugin</a>.
Anytime there is an action on your status page, all of your plugins will be notified of the change with the values that were changed or created.
<p></p>
Using the statup/plugin Golang package you can quickly implement the event listeners. Statup uses <a href="https://github.com/upper/db">upper.io/db.v3</a> for the database connection.
You can use the database inside of your plugin to create, update, and destroy tables/data. <b>Please only use respectable plugins!</b>
# Custom Stlying
On Statup Status Page server can you create your own custom stylesheet to be rendered on the index view of your status page. Go to <a href="/settings">Settings</a> and click on Custom Styling.
# API Endpoints
Statup includes a RESTFUL API so you can view, update, and edit your services with easy to use routes. You can currently view, update and delete services, view, create, update users, and get detailed information about the Statup instance. To make life easy, try out a Postman or Swagger JSON file and use it on your Statup Server.
<p align="center">
<a href="https://github.com/hunterlong/statup/blob/master/dev/postman.json">Postman JSON Export</a> | <a href="https://github.com/hunterlong/statup/blob/master/dev/swagger.json">Swagger Export</a>
</p>
## Authentication
Authentication uses the Statup API Secret to accept remote requests. You can find the API Secret in the Settings page of your Statup server. To send requests to your Statup API, include a Authorization Header when you send the request. The API will accept any one of the headers below.
- HTTP Header: `Authorization: API SECRET HERE`
- HTTP Header: `Authorization: Bearer API SECRET HERE`
## Main Route `/api`
The main API route will show you all services and failures along with them.
## Services
The services API endpoint will show you detailed information about services and will allow you to edit/delete services with POST/DELETE http methods.
### Viewing All Services
- Endpoint: `/api/services`
- Method: `GET`
- Response: Array of [Services](https://github.com/hunterlong/statup/wiki/API#service-response)
- Response Type: `application/json`
- Request Type: `application/json`
### Viewing Service
- Endpoint: `/api/services/{id}`
- Method: `GET`
- Response: [Service](https://github.com/hunterlong/statup/wiki/API#service-response)
- Response Type: `application/json`
- Request Type: `application/json`
### Updating Service
- Endpoint: `/api/services/{id}`
- Method: `POST`
- Response: [Service](https://github.com/hunterlong/statup/wiki/API#service-response)
- Response Type: `application/json`
- Request Type: `application/json`
POST Data:
``` json
{
"name": "Updated Service",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 15,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0
}
```
### Deleting Service
- Endpoint: `/api/services/{id}`
- Method: `DELETE`
- Response: [Object Response](https://github.com/hunterlong/statup/wiki/API#object-response)
- Response Type: `application/json`
- Request Type: `application/json`
Response:
``` json
{
"status": "success",
"id": 4,
"type": "service",
"method": "delete"
}
```
## Users
The users API endpoint will show you users that are registered inside your Statup instance.
### View All Users
- Endpoint: `/api/users`
- Method: `GET`
- Response: Array of [Users](https://github.com/hunterlong/statup/wiki/API#user-response)
- Response Type: `application/json`
- Request Type: `application/json`
### Viewing User
- Endpoint: `/api/users/{id}`
- Method: `GET`
- Response: [User](https://github.com/hunterlong/statup/wiki/API#user-response)
- Response Type: `application/json`
- Request Type: `application/json`
### Creating New User
- Endpoint: `/api/users`
- Method: `POST`
- Response: [User](https://github.com/hunterlong/statup/wiki/API#user-response)
- Response Type: `application/json`
- Request Type: `application/json`
POST Data:
``` json
{
"username": "newadmin",
"email": "info@email.com",
"password": "password123",
"admin": true
}
```
### Updating User
- Endpoint: `/api/users/{id}`
- Method: `POST`
- Response: [User](https://github.com/hunterlong/statup/wiki/API#user-response)
- Response Type: `application/json`
- Request Type: `application/json`
POST Data:
``` json
{
"username": "updatedadmin",
"email": "info@email.com",
"password": "password123",
"admin": true
}
```
### Deleting User
- Endpoint: `/api/services/{id}`
- Method: `DELETE`
- Response: [Object Response](https://github.com/hunterlong/statup/wiki/API#object-response)
- Response Type: `application/json`
- Request Type: `application/json`
Response:
``` json
{
"status": "success",
"id": 3,
"type": "user",
"method": "delete"
}
```
# Service Response
``` json
{
"id": 8,
"name": "Test Service 0",
"domain": "https://status.coinapp.io",
"expected": "",
"expected_status": 200,
"check_interval": 1,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 30,
"order_id": 0,
"created_at": "2018-09-12T09:07:03.045832088-07:00",
"updated_at": "2018-09-12T09:07:03.046114305-07:00",
"online": false,
"latency": 0.031411064,
"24_hours_online": 0,
"avg_response": "",
"status_code": 502,
"last_online": "0001-01-01T00:00:00Z",
"dns_lookup_time": 0.001727175,
"failures": [
{
"id": 5187,
"issue": "HTTP Status Code 502 did not match 200",
"created_at": "2018-09-12T10:41:46.292277471-07:00"
},
{
"id": 5188,
"issue": "HTTP Status Code 502 did not match 200",
"created_at": "2018-09-12T10:41:47.337659862-07:00"
}
]
}
```
# User Response
``` json
{
"id": 1,
"username": "admin",
"api_key": "02f324450a631980121e8fd6ea7dfe4a7c685a2f",
"admin": true,
"created_at": "2018-09-12T09:06:53.906398511-07:00",
"updated_at": "2018-09-12T09:06:54.972440207-07:00"
}
```
# Object Response
``` json
{
"type": "service",
"id": 19,
"method": "delete",
"status": "success"
}
```
# Main API Response
``` json
{
"name": "Awesome Status",
"description": "An awesome status page by Statup",
"footer": "This is my custom footer",
"domain": "https://demo.statup.io",
"version": "v0.56",
"migration_id": 1536768413,
"created_at": "2018-09-12T09:06:53.905374829-07:00",
"updated_at": "2018-09-12T09:07:01.654201225-07:00",
"database": "sqlite",
"started_on": "2018-09-12T10:43:07.760729349-07:00",
"services": [
{
"id": 1,
"name": "Google",
"domain": "https://google.com",
"expected": "",
"expected_status": 200,
"check_interval": 10,
"type": "http",
"method": "GET",
"post_data": "",
"port": 0,
"timeout": 10,
"order_id": 0,
"created_at": "2018-09-12T09:06:54.97549122-07:00",
"updated_at": "2018-09-12T09:06:54.975624103-07:00",
"online": true,
"latency": 0.09080986,
"24_hours_online": 0,
"avg_response": "",
"status_code": 200,
"last_online": "2018-09-12T10:44:07.931990439-07:00",
"dns_lookup_time": 0.005543935
}
]
}
```
# Prometheus Exporter
Statup includes a prometheus exporter so you can have even more monitoring power with your services. The prometheus exporter can be seen on `/metrics`, simply create another exporter in your prometheus config. Use your Statup API Secret for the Authorization Bearer header, the `/metrics` URL is dedicated for Prometheus and requires the correct API Secret has `Authorization` header.
# Grafana Dashboard
Statup has a [Grafana Dashboard](https://grafana.com/dashboards/6950) that you can quickly implement if you've added your Statup service to Prometheus. Import Dashboard ID: `6950` into your Grafana dashboard and watch the metrics come in!
<p align="center"><img width="80%" src="https://img.cjx.io/statupgrafana.png"></p>
## Basic Prometheus Exporter
If you have Statup and the Prometheus server in the same Docker network, you can use the yaml config below.
``` yaml
scrape_configs:
- job_name: 'statup'
scrape_interval: 30s
bearer_token: 'SECRET API KEY HERE'
static_configs:
- targets: ['statup:8080']
```
## Remote URL Prometheus Exporter
This exporter yaml below has `scheme: https`, which you can remove if you arn't using HTTPS.
``` yaml
scrape_configs:
- job_name: 'statup'
scheme: https
scrape_interval: 30s
bearer_token: 'SECRET API KEY HERE'
static_configs:
- targets: ['status.mydomain.com']
```
### `/metrics` Output
```
statup_total_failures 206
statup_total_services 4
statup_service_failures{id="1" name="Google"} 0
statup_service_latency{id="1" name="Google"} 12
statup_service_online{id="1" name="Google"} 1
statup_service_status_code{id="1" name="Google"} 200
statup_service_response_length{id="1" name="Google"} 10777
statup_service_failures{id="2" name="Statup.io"} 0
statup_service_latency{id="2" name="Statup.io"} 3
statup_service_online{id="2" name="Statup.io"} 1
statup_service_status_code{id="2" name="Statup.io"} 200
statup_service_response_length{id="2" name="Statup.io"} 2
```
# Static HTML Exporter
You might have a server that won't allow you to run command that run longer for 60 seconds, or maybe you just want to export your status page to a static HTML file. Using the Statup exporter you can easily do this with 1 command.
```
statup export
```
###### 'index.html' is created in current directory with static CDN url's.
## Push to Github
Once you have the `index.html` file, you could technically send it to an FTP server, Email it, Pastebin it, or even push to your Github repo for Status updates directly from repo.
```bash
git add index.html
git commit -m "Updated Status Page"
git push -u origin/master
```
# Config with .env File
It may be useful to load your environment using a `.env` file in the root directory of your Statup server. The .env file will be automatically loaded on startup and will overwrite all values you have in config.yml.
If you have the `DB_CONN` environment variable set Statup will bypass all values in config.yml and will require you to have the other DB_* variables in place. You can pass in these environment variables without requiring a .env file.
## `.env` File
```bash
DB_CONN=postgres
DB_HOST=0.0.0.0
DB_PORT=5432
DB_USER=root
DB_PASS=password123
DB_DATABASE=root
NAME=Demo
DESCRIPTION=This is an awesome page
DOMAIN=https://domain.com
ADMIN_USER=admin
ADMIN_PASS=admin
ADMIN_EMAIL=info@admin.com
USE_CDN=true
IS_DOCKER=false
IS_AWS=false
SASS=/usr/local/bin/sass
CMD_FILE=/bin/bash
```
This .env file will include additional variables in the future, subscribe to this repo to keep up-to-date with changes and updates.
# Makefile
Here's a simple list of Makefile commands you can run using `make`. The [Makefile](https://github.com/hunterlong/statup/blob/master/Makefile) may change often, so i'll try to keep this Wiki up-to-date.
- Ubuntu `apt-get install build-essential`
- MacOSX `sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer`
- Windows [Install Guide for GNU make utility](http://gnuwin32.sourceforge.net/packages/make.htm)
- CentOS/RedHat `yum groupinstall "Development Tools"`
### Commands
``` bash
make build # build the binary
make install
make run
make test
make coverage
make docs
# Building Statup
make build-all
make build-alpine
make docker
make docker-run
make docker-dev
make docker-run-dev
make databases
make dep
make dev-deps
make clean
make compress
make cypress-install
make cypress-test
```
## Testing
* If you want to test your updates with the current golang testing units, you can follow the guide below to run a full test process. Each test for Statup will run in MySQL, Postgres, and SQlite to make sure all database types work correctly.
## Create Docker Databases
The easiest way to run the tests on all 3 databases is by starting temporary databases servers with Docker. Docker is available for Linux, Mac and Windows. You can download/install it by going to the [Docker Installation](https://docs.docker.com/install/) site.
``` bash
docker run -it -d \
-p 3306:3306 \
-env MYSQL_ROOT_PASSWORD=password123 \
-env MYSQL_DATABASE=root mysql
```
``` bash
docker run -it -d \
-p 5432:5432 \
-env POSTGRES_PASSWORD=password123 \
-env POSTGRES_USER=root \
-env POSTGRES_DB=root postgres
```
Once you have MySQL and Postgres running, you can begin the testing. SQLite database will automatically create a `statup.db` file and will delete after testing.
## Run Tests
Insert the database environment variables to auto connect the the databases and run the normal test command: `go test -v`. You'll see a verbose output of each test. If all tests pass, make a push request! 💃
``` bash
DB_DATABASE=root \
DB_USER=root \
DB_PASS=password123 \
DB_HOST=localhost \
go test -v
```

View File

@ -135,11 +135,14 @@
{{ range .Notifications }}
{{$n := .Select}}
<div class="tab-pane" id="v-pills-{{underscore $n.Method}}" role="tabpanel" aria-labelledby="v-pills-{{underscore $n.Method }}-tab">
{{if $n.Title}}<h4>{{$n.Title}}</h4>{{end}}
{{if $n.Description}}<p class="text-muted">{{safe $n.Description}}</p>{{end}}
<form method="POST" action="/settings/notifier/{{ $n.Method }}">
{{range .Form}}
<div class="form-group">
<label class="text-capitalize" for="{{underscore .Title}}">{{.Title}}</label>
<input type="{{.Type}}" name="{{underscore .DbField}}" class="form-control" value="{{ $n.GetValue .DbField }}" id="{{underscore .Title}}" placeholder="{{.Placeholder}}">
{{if .SmallText}}<small class="form-text text-muted">{{safe .SmallText}}</small>{{end}}
</div>
{{end}}
@ -160,10 +163,15 @@
</div>
</div>
{{if $n.Author}}
<span class="d-block small text-center">
{{$n.Title}} Notifier created by <a href="{{$n.AuthorUrl}}" target="_blank">{{$n.Author}}</a>
</span>
{{ end }}
</form>
{{ if $n.Logs }}
Sent {{$n.SentLastHour}} out of {{$n.LimitValue}} in the last hour<br>
Sent {{$n.SentLastHour}} out of {{$n.Limit}} in the last hour<br>
{{ range $n.Logs }}
<div class="card mt-1">
<div class="card-body">

View File

@ -53,7 +53,7 @@ type ServiceInterface interface {
Select() *Service
CheckQueue(bool)
Check(bool)
Create() (int64, error)
Create(bool) (int64, error)
Update(bool) error
Delete() error
}

30
types/time.go Normal file
View File

@ -0,0 +1,30 @@
// Statup
// Copyright (C) 2018. Hunter Long and the project contributors
// Written by Hunter Long <info@socialeck.com> and the project contributors
//
// https://github.com/hunterlong/statup
//
// The licenses for most software and other practical works are designed
// to take away your freedom to share and change the works. By contrast,
// the GNU General Public License is intended to guarantee your freedom to
// share and change all versions of a program--to make sure it remains free
// software for all its users.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package types
import (
"time"
)
var (
NOW = func() time.Time { return time.Now() }()
HOUR_1_AGO = time.Now().Add(-1 * time.Hour)
HOUR_24_AGO = time.Now().Add(-24 * time.Hour)
HOUR_72_AGO = time.Now().Add(-72 * time.Hour)
DAY_7_AGO = NOW.AddDate(0, 0, -7)
MONTH_1_AGO = NOW.AddDate(0, -1, 0)
YEAR_1_AGO = NOW.AddDate(-1, 0, 0)
)