From 36c912d23ad6c9f32a32379e7190660ff664a27d Mon Sep 17 00:00:00 2001 From: Hunter Long Date: Fri, 14 Sep 2018 18:18:21 -0700 Subject: [PATCH] fixed notification - additional tests - api changes --- Gopkg.lock | 34 ++- Gopkg.toml | 10 +- Makefile | 2 +- cmd/main.go | 1 + cmd/main_test.go | 67 ++--- core/checker.go | 4 +- core/configs.go | 14 +- core/core.go | 3 +- core/core_test.go | 4 +- core/database.go | 30 +-- core/notifier/audit.go | 2 +- core/notifier/events.go | 28 +-- core/notifier/example_test.go | 86 ++++--- core/notifier/interfaces.go | 8 +- core/notifier/notifiers.go | 220 ++++++++++------ core/notifier/notifiers_test.go | 239 +++++++++++++----- core/sample.go | 301 ++++++++++++++++++++++ core/services.go | 21 +- core/services_test.go | 46 ++-- core/setup.go | 127 ---------- core/users_test.go | 10 +- dev/notifier/example.go | 127 ---------- handlers/api.go | 18 +- handlers/api_handlers_test.go | 25 +- handlers/dashboard.go | 4 +- handlers/handlers_test.go | 9 +- handlers/index.go | 2 +- handlers/routes.go | 1 + handlers/services.go | 2 +- handlers/settings.go | 3 - handlers/setup.go | 2 +- notifiers/discord.go | 54 ++-- notifiers/email.go | 200 ++++++--------- notifiers/line_notify.go | 82 ++---- notifiers/rice-box.go | 63 ----- notifiers/slack.go | 109 ++++---- notifiers/twilio.go | 76 +++--- source/js/charts.js | 2 +- source/source.go | 11 + source/tmpl/help.html | 43 ++-- source/tmpl/help.md | 434 ++++++++++++++++++++++++++++++++ source/tmpl/settings.html | 10 +- types/service.go | 2 +- types/time.go | 30 +++ 44 files changed, 1581 insertions(+), 985 deletions(-) create mode 100644 core/sample.go delete mode 100644 core/setup.go delete mode 100644 dev/notifier/example.go delete mode 100644 notifiers/rice-box.go create mode 100644 source/tmpl/help.md create mode 100644 types/time.go diff --git a/Gopkg.lock b/Gopkg.lock index 3df9541a..34d53da3 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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" diff --git a/Gopkg.toml b/Gopkg.toml index 103c2ea8..7f1737e3 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -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" diff --git a/Makefile b/Makefile index 07c1595e..ff5e7542 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -VERSION=0.59 +VERSION=0.6 BINARY_NAME=statup GOPATH:=$(GOPATH) GOCMD=go diff --git a/cmd/main.go b/cmd/main.go index 8fe2ebb9..6a39f606 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -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" diff --git a/cmd/main_test.go b/cmd/main_test.go index f91b1b81..caec2baa 100644 --- a/cmd/main_test.go +++ b/cmd/main_test.go @@ -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)) } diff --git a/core/checker.go b/core/checker.go index 04859026..45933cf4 100644 --- a/core/checker.go +++ b/core/checker.go @@ -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() diff --git a/core/configs.go b/core/configs.go index f0953baa..67d58c62 100644 --- a/core/configs.go +++ b/core/configs.go @@ -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 +} diff --git a/core/core.go b/core/core.go index 424a8599..e2e4e257 100644 --- a/core/core.go +++ b/core/core.go @@ -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() } diff --git a/core/core_test.go b/core/core_test.go index ea54965d..6c69ef49 100644 --- a/core/core_test.go +++ b/core/core_test.go @@ -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) { diff --git a/core/database.go b/core/database.go index 81925f39..5ee06b53 100644 --- a/core/database.go +++ b/core/database.go @@ -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 { diff --git a/core/notifier/audit.go b/core/notifier/audit.go index dfbb49b5..0a3ea413 100644 --- a/core/notifier/audit.go +++ b/core/notifier/audit.go @@ -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 { diff --git a/core/notifier/events.go b/core/notifier/events.go index 6aca6fd1..65a44ff5 100644 --- a/core/notifier/events.go +++ b/core/notifier/events.go @@ -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) } } diff --git a/core/notifier/example_test.go b/core/notifier/example_test.go index 50b42137..0e635418 100644 --- a/core/notifier/example_test.go +++ b/core/notifier/example_test.go @@ -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) } diff --git a/core/notifier/interfaces.go b/core/notifier/interfaces.go index 145da435..b09fb5ab 100644 --- a/core/notifier/interfaces.go +++ b/core/notifier/interfaces.go @@ -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 diff --git a/core/notifier/notifiers.go b/core/notifier/notifiers.go index be86bb19..f5363848 100644 --- a/core/notifier/notifiers.go +++ b/core/notifier/notifiers.go @@ -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(¬ifier) - return ¬ifier, err.Error +func SelectNotification(n Notifier) (*Notification, error) { + notifier := n.Select() + err := db.Model(&Notification{}).Where("method = ?", notifier.Method).Scan(¬ifier) + 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 } diff --git a/core/notifier/notifiers_test.go b/core/notifier/notifiers_test.go index a1176b46..14a045d2 100644 --- a/core/notifier/notifiers_test.go +++ b/core/notifier/notifiers_test.go @@ -1,5 +1,3 @@ -// +build bypass - // Statup // Copyright (C) 2018. Hunter Long and the project contributors // Written by Hunter Long 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) } diff --git a/core/sample.go b/core/sample.go new file mode 100644 index 00000000..62038e32 --- /dev/null +++ b/core/sample.go @@ -0,0 +1,301 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long 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 . + +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) + } + + } + +} diff --git a/core/services.go b/core/services.go index 017e2b7c..60ca0c67 100644 --- a/core/services.go +++ b/core/services.go @@ -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) diff --git a/core/services_test.go b/core/services_test.go index bf84a42f..190ebdf7 100644 --- a/core/services_test.go +++ b/core/services_test.go @@ -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) { diff --git a/core/setup.go b/core/setup.go deleted file mode 100644 index f2b1c370..00000000 --- a/core/setup.go +++ /dev/null @@ -1,127 +0,0 @@ -// Statup -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long 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 . - -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 -} diff --git a/core/users_test.go b/core/users_test.go index 647677ac..b61e9787 100644 --- a/core/users_test.go +++ b/core/users_test.go @@ -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) } diff --git a/dev/notifier/example.go b/dev/notifier/example.go deleted file mode 100644 index 7e4b94c3..00000000 --- a/dev/notifier/example.go +++ /dev/null @@ -1,127 +0,0 @@ -// +build test - -// Statup -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long 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 . - -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{¬ifiers.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 -} diff --git a/handlers/api.go b/handlers/api.go index 0799f09b..fa502b80 100644 --- a/handlers/api.go +++ b/handlers/api.go @@ -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 diff --git a/handlers/api_handlers_test.go b/handlers/api_handlers_test.go index c2aa3c98..a848236e 100644 --- a/handlers/api_handlers_test.go +++ b/handlers/api_handlers_test.go @@ -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) } diff --git a/handlers/dashboard.go b/handlers/dashboard.go index 1f2915f5..6fa3d12c 100644 --- a/handlers/dashboard.go +++ b/handlers/dashboard.go @@ -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) { diff --git a/handlers/handlers_test.go b/handlers/handlers_test.go index 985a99a1..7492af79 100644 --- a/handlers/handlers_test.go +++ b/handlers/handlers_test.go @@ -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() diff --git a/handlers/index.go b/handlers/index.go index 579a19b4..b3356dc0 100644 --- a/handlers/index.go +++ b/handlers/index.go @@ -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 diff --git a/handlers/routes.go b/handlers/routes.go index f7e7429b..2b3927f5 100644 --- a/handlers/routes.go +++ b/handlers/routes.go @@ -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") diff --git a/handlers/services.go b/handlers/services.go index 3adc5604..1a3dd363 100644 --- a/handlers/services.go +++ b/handlers/services.go @@ -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)) } diff --git a/handlers/settings.go b/handlers/settings.go index d41aa586..10752c9f 100644 --- a/handlers/settings.go +++ b/handlers/settings.go @@ -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"))) diff --git a/handlers/setup.go b/handlers/setup.go index 591fbede..1562abf5 100644 --- a/handlers/setup.go +++ b/handlers/setup.go @@ -139,7 +139,7 @@ func ProcessSetupHandler(w http.ResponseWriter, r *http.Request) { admin.Create() if sample == "on" { - core.LoadSampleData() + core.InsertSampleData() } core.InitApp() diff --git a/notifiers/discord.go b/notifiers/discord.go index 22a7669e..bef0f4bf 100644 --- a/notifiers/discord.go +++ b/notifiers/discord.go @@ -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{¬ifier.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 Discord Webhook API.", + 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 } diff --git a/notifiers/email.go b/notifiers/email.go index 4375a059..9c453121 100644 --- a/notifiers/email.go +++ b/notifiers/email.go @@ -18,68 +18,65 @@ package notifiers import ( "bytes" "fmt" - "github.com/GeertJohan/go.rice" "github.com/hunterlong/statup/core/notifier" "github.com/hunterlong/statup/types" "github.com/hunterlong/statup/utils" "gopkg.in/gomail.v2" "html/template" - "time" ) const ( - EMAIL_ID int64 = 1 - EMAIL_METHOD = "email" + MESSAGE = "\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

Looks Like Emails Work!

\n

\n Since you got this email, it confirms that your Statup Status Page email system is working correctly.\n

\n

\n

\n Enjoy using Statup!\n
Statup.io Team

\n\n \n\n
\n
\n
\n
\n\n" + FAILURE = "\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

{{ .Service.Name }} is Offline!

\n

\n Your Statup service '{{.Service.Name}}' has been triggered with a HTTP status code of '{{.Service.LastStatusCode}}' and is currently offline based on your requirements. This failure was created on {{.Service.CreatedAt}}.\n

\n\n {{if .Service.LastResponse }}\n

Last Response

\n

\n {{ .Service.LastResponse }}\n

\n {{end}}\n\n \n \n \n
\n View Service\n \n Statup Dashboard\n
\n
\n
\n
\n\n" ) var ( - emailArray []string - emailQueue []*EmailOutgoing - emailBox *rice.Box - mailer *gomail.Dialer + mailer *gomail.Dialer ) type Email struct { *notifier.Notification } -var emailer = &Email{ - Notification: ¬ifier.Notification{ - Method: EMAIL_METHOD, - Form: []notifier.NotificationForm{{ - Type: "text", - Title: "SMTP Host", - Placeholder: "Insert your SMTP Host here.", - DbField: "Host", - }, { - Type: "text", - Title: "SMTP Username", - Placeholder: "Insert your SMTP Username here.", - DbField: "Username", - }, { - Type: "password", - Title: "SMTP Password", - Placeholder: "Insert your SMTP Password here.", - DbField: "Password", - }, { - Type: "number", - Title: "SMTP Port", - Placeholder: "Insert your SMTP Port here.", - DbField: "Port", - }, { - Type: "text", - Title: "Outgoing Email Address", - Placeholder: "Insert your Outgoing Email Address", - DbField: "Var1", - }, { - Type: "email", - Title: "Send Alerts To", - Placeholder: "Email Address", - DbField: "Var2", - }}, - }} +var emailer = &Email{¬ifier.Notification{ + Method: "email", + Title: "Email", + Description: "Send emails via SMTP when notification are online or offline.", + Author: "Hunter Long", + AuthorUrl: "https://github.com/hunterlong", + Form: []notifier.NotificationForm{{ + Type: "text", + Title: "SMTP Host", + Placeholder: "Insert your SMTP Host here.", + DbField: "Host", + }, { + Type: "text", + Title: "SMTP Username", + Placeholder: "Insert your SMTP Username here.", + DbField: "Username", + }, { + Type: "password", + Title: "SMTP Password", + Placeholder: "Insert your SMTP Password here.", + DbField: "Password", + }, { + Type: "number", + Title: "SMTP Port", + Placeholder: "Insert your SMTP Port here.", + DbField: "Port", + }, { + Type: "text", + Title: "Outgoing Email Address", + Placeholder: "Insert your Outgoing Email Address", + DbField: "Var1", + }, { + Type: "email", + Title: "Send Alerts To", + Placeholder: "Email Address", + DbField: "Var2", + }}, +}} -// DEFINE YOUR NOTIFICATION HERE. func init() { err := notifier.AddNotifier(emailer) if err != nil { @@ -87,23 +84,26 @@ func init() { } } -func (u *Email) Test() error { - utils.Log(1, "Emailer notifier loaded") - if u.Enabled { - email := &EmailOutgoing{ - To: emailer.Var2, - Subject: "Test Email", - Template: "message.html", - Data: nil, - From: emailer.Var1, - } - SendEmail(emailBox, email) - } +func (u *Email) Send(msg interface{}) error { + //email := msg.(*EmailOutgoing) + //err := u.dialSend(email) + //if err != nil { + // utils.Log(3, fmt.Sprintf("Email Notifier could not send email: %v", err)) + // return err + //} return nil } -type emailMessage struct { - Service *types.Service +func (u *Email) Test() error { + email := &EmailOutgoing{ + To: emailer.GetValue("var2"), + Subject: "Test Email", + Template: MESSAGE, + Data: nil, + From: emailer.GetValue("var1"), + } + u.AddQueue(email) + return nil } type EmailOutgoing struct { @@ -116,50 +116,16 @@ type EmailOutgoing struct { Sent bool } -// AFTER NOTIFIER LOADS, IF ENABLED, START A QUEUE PROCESS -func (u *Email) Run() error { - var sentAddresses []string - for _, email := range emailQueue { - if inArray(sentAddresses, email.To) || email.Sent { - emailQueue = removeEmail(emailQueue, email) - continue - } - if u.CanSend() { - err := u.dialSend(email) - if err == nil { - email.Sent = true - sentAddresses = append(sentAddresses, email.To) - utils.Log(1, fmt.Sprintf("Email '%v' sent to: %v using the %v template (size: %v)", email.Subject, email.To, email.Template, len([]byte(email.Source)))) - emailQueue = removeEmail(emailQueue, email) - u.Log(fmt.Sprintf("Subject: %v to %v", email.Subject, email.To)) - } else { - utils.Log(3, fmt.Sprintf("Email Notifier could not send email: %v", err)) - } - } - } - time.Sleep(60 * time.Second) - if u.Enabled { - return u.Run() - } - return nil -} - // ON SERVICE FAILURE, DO YOUR OWN FUNCTIONS func (u *Email) OnFailure(s *types.Service, f *types.Failure) { - if u.Enabled { - msg := emailMessage{ - Service: s, - } - email := &EmailOutgoing{ - To: emailer.Var2, - Subject: fmt.Sprintf("Service %v is Failing", s.Name), - Template: "failure.html", - Data: msg, - From: emailer.Var1, - } - SendEmail(emailBox, email) - + email := &EmailOutgoing{ + To: emailer.GetValue("var2"), + Subject: fmt.Sprintf("Service %v is Failing", s.Name), + Template: FAILURE, + Data: interface{}(s), + From: emailer.GetValue("var1"), } + u.AddQueue(email) } // ON SERVICE SUCCESS, DO YOUR OWN FUNCTIONS @@ -167,6 +133,10 @@ func (u *Email) OnSuccess(s *types.Service) { } +func (u *Email) Select() *notifier.Notification { + return u.Notification +} + // ON SAVE OR UPDATE OF THE NOTIFIER FORM func (u *Email) OnSave() error { utils.Log(1, fmt.Sprintf("Notification %v is receiving updated information.", u.Method)) @@ -187,19 +157,14 @@ func (u *Email) dialSend(email *EmailOutgoing) error { return nil } -func SendEmail(box *rice.Box, email *EmailOutgoing) { - source := EmailTemplate(box, email.Template, email.Data) +func SendEmail(email *EmailOutgoing) { + source := EmailTemplate(email.Template, email.Data) email.Source = source - emailQueue = append(emailQueue, email) } -func EmailTemplate(box *rice.Box, tmpl string, data interface{}) string { - emailTpl, err := box.String(tmpl) - if err != nil { - utils.Log(3, err) - } +func EmailTemplate(contents string, data interface{}) string { t := template.New("email") - t, err = t.Parse(emailTpl) + t, err := t.Parse(contents) if err != nil { utils.Log(3, err) } @@ -210,22 +175,3 @@ func EmailTemplate(box *rice.Box, tmpl string, data interface{}) string { result := tpl.String() return result } - -func removeEmail(emails []*EmailOutgoing, em *EmailOutgoing) []*EmailOutgoing { - var newArr []*EmailOutgoing - for _, e := range emails { - if e != em { - newArr = append(newArr, e) - } - } - return newArr -} - -func inArray(a []string, v string) bool { - for _, i := range a { - if i == v { - return true - } - } - return false -} diff --git a/notifiers/line_notify.go b/notifiers/line_notify.go index c8fcce34..a6b02cdd 100644 --- a/notifiers/line_notify.go +++ b/notifiers/line_notify.go @@ -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{¬ifier.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 LINE Notify API.", + Author: "Kanin Peanviriyakulkit", + AuthorUrl: "https://github.com/dogrocker", Form: []notifier.NotificationForm{{ Type: "text", Title: "Access Token", @@ -53,66 +51,40 @@ var lineNotify = &LineNotify{¬ifier.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 diff --git a/notifiers/rice-box.go b/notifiers/rice-box.go deleted file mode 100644 index 7ee585e4..00000000 --- a/notifiers/rice-box.go +++ /dev/null @@ -1,63 +0,0 @@ -// Statup -// Copyright (C) 2018. Hunter Long and the project contributors -// Written by Hunter Long 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 . - -package notifiers - -import ( - "github.com/GeertJohan/go.rice/embedded" - "time" -) - -func init() { - - // define files - file2 := &embedded.EmbeddedFile{ - Filename: "failure.html", - FileModTime: time.Unix(1531720141, 0), - Content: string("\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

{{ .Service.Name }} is Offline!

\n

\n Your Statup service '{{.Service.Name}}' has been triggered with a HTTP status code of '{{.Service.LastStatusCode}}' and is currently offline based on your requirements. This failure was created on {{.Service.CreatedAt}}.\n

\n\n {{if .Service.LastResponse }}\n

Last Response

\n

\n {{ .Service.LastResponse }}\n

\n {{end}}\n\n \n \n \n
\n View Service\n \n Statup Dashboard\n
\n
\n
\n
\n\n"), - } - file3 := &embedded.EmbeddedFile{ - Filename: "message.html", - FileModTime: time.Unix(1530546686, 0), - Content: string("\n\n\n \n \n Sample Email\n\n\n\n\n\n \n \n \n
\n \n\n \n \n \n
\n \n\n \n \n \n
\n

Looks Like Emails Work!

\n

\n Since you got this email, it confirms that your Statup Status Page email system is working correctly.\n

\n

\n

\n Enjoy using Statup!\n
Statup.io Team

\n\n \n\n
\n
\n
\n
\n\n"), - } - - // define dirs - dir1 := &embedded.EmbeddedDir{ - Filename: "", - DirModTime: time.Unix(1531720141, 0), - ChildFiles: []*embedded.EmbeddedFile{ - file2, // "failure.html" - file3, // "message.html" - - }, - } - - // link ChildDirs - dir1.ChildDirs = []*embedded.EmbeddedDir{} - - // register embeddedBox - embedded.RegisterEmbeddedBox(`emails`, &embedded.EmbeddedBox{ - Name: `emails`, - Time: time.Unix(1531720141, 0), - Dirs: map[string]*embedded.EmbeddedDir{ - "": dir1, - }, - Files: map[string]*embedded.EmbeddedFile{ - "failure.html": file2, - "message.html": file3, - }, - }) -} diff --git a/notifiers/slack.go b/notifiers/slack.go index e8de58b7..e677ae7c 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -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{¬ifier.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 Slack API.", + 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 Slack Apps", 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 } diff --git a/notifiers/twilio.go b/notifiers/twilio.go index 2bd268de..150bf577 100644 --- a/notifiers/twilio.go +++ b/notifiers/twilio.go @@ -40,7 +40,12 @@ type Twilio struct { } var twilio = &Twilio{¬ifier.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 Twilio API.", + 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 diff --git a/source/js/charts.js b/source/js/charts.js index 07ac03ff..5dbb1b6e 100644 --- a/source/js/charts.js +++ b/source/js/charts.js @@ -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 }} \ No newline at end of file +{{ end }} diff --git a/source/source.go b/source/source.go index 54322393..171631a6 100644 --- a/source/source.go +++ b/source/source.go @@ -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") diff --git a/source/tmpl/help.html b/source/tmpl/help.html index ae470930..c2058256 100644 --- a/source/tmpl/help.html +++ b/source/tmpl/help.html @@ -21,38 +21,23 @@ {{end}}
- -

Statup v{{ VERSION }} 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! - -

- - - -

- -

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. - -

Users

- Users can access the Statup Dashboard to add, remove, and view services. - -

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 https://github.com/hunterlong/statup_plugin. - 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. -

- Using the statup/plugin Golang package you can quickly implement the event listeners. Statup uses upper.io/db.v3 for the database connection. - You can use the database inside of your plugin to create, update, and destroy tables/data. Please only use respectable plugins! - -

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 Settings and click on Custom Styling. - + {{ safe . }}
+ + {{template "footer"}} {{if USE_CDN}} @@ -66,4 +51,4 @@ {{end}} - \ No newline at end of file + diff --git a/source/tmpl/help.md b/source/tmpl/help.md new file mode 100644 index 00000000..d0021258 --- /dev/null +++ b/source/tmpl/help.md @@ -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! + +

+ + + +

+ +# 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 https://github.com/hunterlong/statup_plugin. +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. +

+Using the statup/plugin Golang package you can quickly implement the event listeners. Statup uses upper.io/db.v3 for the database connection. +You can use the database inside of your plugin to create, update, and destroy tables/data. Please only use respectable plugins! + +# 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 Settings 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. + +

+Postman JSON Export | Swagger Export +

+ +## 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! + +

+ +## 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 +``` diff --git a/source/tmpl/settings.html b/source/tmpl/settings.html index cbf4ca94..37c98dfb 100644 --- a/source/tmpl/settings.html +++ b/source/tmpl/settings.html @@ -135,11 +135,14 @@ {{ range .Notifications }} {{$n := .Select}}
+ {{if $n.Title}}

{{$n.Title}}

{{end}} + {{if $n.Description}}

{{safe $n.Description}}

{{end}}
{{range .Form}}
+ {{if .SmallText}}{{safe .SmallText}}{{end}}
{{end}} @@ -160,10 +163,15 @@
+ {{if $n.Author}} + + {{$n.Title}} Notifier created by {{$n.Author}} + + {{ end }} {{ if $n.Logs }} - Sent {{$n.SentLastHour}} out of {{$n.LimitValue}} in the last hour
+ Sent {{$n.SentLastHour}} out of {{$n.Limit}} in the last hour
{{ range $n.Logs }}
diff --git a/types/service.go b/types/service.go index af1eed2e..dd107055 100644 --- a/types/service.go +++ b/types/service.go @@ -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 } diff --git a/types/time.go b/types/time.go new file mode 100644 index 00000000..3693786f --- /dev/null +++ b/types/time.go @@ -0,0 +1,30 @@ +// Statup +// Copyright (C) 2018. Hunter Long and the project contributors +// Written by Hunter Long 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 . + +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) +)