diff --git a/agent/cmd/agent/main.go b/agent/cmd/agent/main.go new file mode 100644 index 0000000..5343f13 --- /dev/null +++ b/agent/cmd/agent/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "fmt" + "net/http" + "time" + + "github.com/corecontrol/agent/internal/app" + "github.com/corecontrol/agent/internal/database" + "github.com/corecontrol/agent/internal/notifications" + "github.com/corecontrol/agent/internal/server" +) + +func main() { + // Initialize database + db, err := database.InitDB() + if err != nil { + panic(fmt.Sprintf("Database initialization failed: %v\n", err)) + } + defer db.Close() + + // Initialize notification sender + notifSender := notifications.NewNotificationSender() + + // Initial load of notifications + notifs, err := database.LoadNotifications(db) + if err != nil { + panic(fmt.Sprintf("Failed to load notifications: %v", err)) + } + notifSender.UpdateNotifications(notifs) + + // Reload notification configs every minute + go func() { + reloadTicker := time.NewTicker(time.Minute) + defer reloadTicker.Stop() + + for range reloadTicker.C { + newNotifs, err := database.LoadNotifications(db) + if err != nil { + fmt.Printf("Failed to reload notifications: %v\n", err) + continue + } + notifSender.UpdateNotifications(newNotifs) + fmt.Println("Reloaded notification configurations") + } + }() + + // Clean up old entries hourly + go func() { + deletionTicker := time.NewTicker(time.Hour) + defer deletionTicker.Stop() + + for range deletionTicker.C { + if err := database.DeleteOldEntries(db); err != nil { + fmt.Printf("Error deleting old entries: %v\n", err) + } + } + }() + + // Check for test notifications every 10 seconds + go func() { + testNotifTicker := time.NewTicker(10 * time.Second) + defer testNotifTicker.Stop() + + for range testNotifTicker.C { + notifs := notifSender.GetNotifications() + database.CheckAndSendTestNotifications(db, notifs, notifSender.SendSpecificNotification) + } + }() + + // HTTP clients + appClient := &http.Client{ + Timeout: 4 * time.Second, + } + + serverClient := &http.Client{ + Timeout: 5 * time.Second, + } + + // Server monitoring every 5 seconds + go func() { + serverTicker := time.NewTicker(5 * time.Second) + defer serverTicker.Stop() + + for range serverTicker.C { + servers, err := database.GetServers(db) + if err != nil { + fmt.Printf("Error getting servers: %v\n", err) + continue + } + server.MonitorServers(db, serverClient, servers, notifSender) + } + }() + + // Application monitoring every 10 seconds + appTicker := time.NewTicker(time.Second) + defer appTicker.Stop() + + for now := range appTicker.C { + if now.Second()%10 != 0 { + continue + } + + apps, err := database.GetApplications(db) + if err != nil { + fmt.Printf("Error getting applications: %v\n", err) + continue + } + app.MonitorApplications(db, appClient, apps, notifSender) + } +} diff --git a/agent/go.mod b/agent/go.mod index 98872ec..a991b61 100644 --- a/agent/go.mod +++ b/agent/go.mod @@ -1,22 +1,22 @@ -module agent +module github.com/corecontrol/agent -go 1.24.1 +go 1.19 require ( - github.com/jackc/pgx/v4 v4.18.3 + github.com/jackc/pgx/v4 v4.18.1 github.com/joho/godotenv v1.5.1 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df ) require ( github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect + github.com/jackc/pgconn v1.14.0 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect + github.com/jackc/pgproto3/v2 v2.3.2 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgtype v1.14.0 // indirect - golang.org/x/crypto v0.31.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.6.0 // indirect + golang.org/x/text v0.7.0 // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect ) diff --git a/agent/go.sum b/agent/go.sum index cec31da..dc0a3fd 100644 --- a/agent/go.sum +++ b/agent/go.sum @@ -25,8 +25,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= +github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q= +github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= @@ -42,8 +42,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= +github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0= +github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= @@ -57,11 +57,12 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08 github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= +github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0= +github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE= github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= +github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= @@ -98,13 +99,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -126,17 +132,22 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -149,15 +160,21 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -165,7 +182,9 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/agent/internal/app/monitor.go b/agent/internal/app/monitor.go new file mode 100644 index 0000000..c4f89a2 --- /dev/null +++ b/agent/internal/app/monitor.go @@ -0,0 +1,123 @@ +package app + +import ( + "context" + "crypto/x509" + "database/sql" + "errors" + "fmt" + "net" + "net/http" + "net/url" + "strings" + "time" + + "github.com/corecontrol/agent/internal/models" + "github.com/corecontrol/agent/internal/notifications" +) + +// MonitorApplications checks and updates the status of all applications +func MonitorApplications(db *sql.DB, client *http.Client, apps []models.Application, notifSender *notifications.NotificationSender) { + var notificationTemplate string + err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate) + if err != nil || notificationTemplate == "" { + notificationTemplate = "The application !name (!url) went !status!" + } + + for _, app := range apps { + logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL) + fmt.Printf("%s Checking...\n", logPrefix) + + parsedURL, parseErr := url.Parse(app.PublicURL) + if parseErr != nil { + fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr) + continue + } + + hostIsIP := isIPAddress(parsedURL.Hostname()) + var isOnline bool + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + req, err := http.NewRequestWithContext(ctx, "GET", app.PublicURL, nil) + if err != nil { + fmt.Printf("%s Request creation failed: %v\n", logPrefix, err) + continue + } + + resp, err := client.Do(req) + if err == nil { + defer resp.Body.Close() + isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400 + fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode) + } else { + fmt.Printf("%s Connection error: %v\n", logPrefix, err) + + if hostIsIP { + var urlErr *url.Error + if errors.As(err, &urlErr) { + var certErr x509.HostnameError + var unknownAuthErr x509.UnknownAuthorityError + if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) { + fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix) + isOnline = true + } + } + } + } + + if isOnline != app.Online { + status := "offline" + if isOnline { + status = "online" + } + + message := strings.ReplaceAll(notificationTemplate, "!name", app.Name) + message = strings.ReplaceAll(message, "!url", app.PublicURL) + message = strings.ReplaceAll(message, "!status", status) + + notifSender.SendNotifications(message) + } + + // Update application status in database + updateApplicationStatus(db, app.ID, isOnline) + + // Add entry to uptime history + addUptimeHistoryEntry(db, app.ID, isOnline) + } +} + +// Helper function to update application status +func updateApplicationStatus(db *sql.DB, appID int, online bool) { + dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer dbCancel() + + _, err := db.ExecContext(dbCtx, + `UPDATE application SET online = $1 WHERE id = $2`, + online, appID, + ) + if err != nil { + fmt.Printf("DB update failed for app ID %d: %v\n", appID, err) + } +} + +// Helper function to add uptime history entry +func addUptimeHistoryEntry(db *sql.DB, appID int, online bool) { + dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) + defer dbCancel() + + _, err := db.ExecContext(dbCtx, + `INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`, + appID, online, + ) + if err != nil { + fmt.Printf("History insert failed for app ID %d: %v\n", appID, err) + } +} + +// Helper function to check if a host is an IP address +func isIPAddress(host string) bool { + ip := net.ParseIP(host) + return ip != nil +} diff --git a/agent/internal/database/database.go b/agent/internal/database/database.go new file mode 100644 index 0000000..91c804f --- /dev/null +++ b/agent/internal/database/database.go @@ -0,0 +1,198 @@ +package database + +import ( + "context" + "database/sql" + "fmt" + "os" + "time" + + "github.com/corecontrol/agent/internal/models" + + _ "github.com/jackc/pgx/v4/stdlib" + "github.com/joho/godotenv" +) + +// InitDB initializes the database connection +func InitDB() (*sql.DB, error) { + // Load environment variables + if err := godotenv.Load(); err != nil { + fmt.Println("No env vars found") + } + + dbURL := os.Getenv("DATABASE_URL") + if dbURL == "" { + return nil, fmt.Errorf("DATABASE_URL not set") + } + + db, err := sql.Open("pgx", dbURL) + if err != nil { + return nil, fmt.Errorf("database connection failed: %v", err) + } + + return db, nil +} + +// GetApplications fetches all applications with public URLs +func GetApplications(db *sql.DB) ([]models.Application, error) { + rows, err := db.Query( + `SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`, + ) + if err != nil { + return nil, fmt.Errorf("error fetching applications: %v", err) + } + defer rows.Close() + + var apps []models.Application + for rows.Next() { + var app models.Application + if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil { + fmt.Printf("Error scanning row: %v\n", err) + continue + } + apps = append(apps, app) + } + return apps, nil +} + +// GetServers fetches all servers with monitoring enabled +func GetServers(db *sql.DB) ([]models.Server, error) { + rows, err := db.Query( + `SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage" + FROM server WHERE monitoring = true`, + ) + if err != nil { + return nil, fmt.Errorf("error fetching servers: %v", err) + } + defer rows.Close() + + var servers []models.Server + for rows.Next() { + var server models.Server + if err := rows.Scan( + &server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL, + &server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage, + ); err != nil { + fmt.Printf("Error scanning server row: %v\n", err) + continue + } + servers = append(servers, server) + } + return servers, nil +} + +// LoadNotifications loads all enabled notifications +func LoadNotifications(db *sql.DB) ([]models.Notification, error) { + rows, err := db.Query( + `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", + "telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken", + "pushoverUrl", "pushoverToken", "pushoverUser" + FROM notification + WHERE enabled = true`, + ) + if err != nil { + return nil, err + } + defer rows.Close() + + var configs []models.Notification + for rows.Next() { + var n models.Notification + if err := rows.Scan( + &n.ID, &n.Enabled, &n.Type, + &n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo, + &n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, + &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken, + &n.PushoverUrl, &n.PushoverToken, &n.PushoverUser, + ); err != nil { + fmt.Printf("Error scanning notification: %v\n", err) + continue + } + configs = append(configs, n) + } + return configs, nil +} + +// DeleteOldEntries removes entries older than 30 days +func DeleteOldEntries(db *sql.DB) error { + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // Delete old uptime history entries + res, err := db.ExecContext(ctx, + `DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`, + ) + if err != nil { + return err + } + affected, _ := res.RowsAffected() + fmt.Printf("Deleted %d old entries from uptime_history\n", affected) + + // Delete old server history entries + res, err = db.ExecContext(ctx, + `DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`, + ) + if err != nil { + return err + } + affected, _ = res.RowsAffected() + fmt.Printf("Deleted %d old entries from server_history\n", affected) + + return nil +} + +// UpdateServerStatus updates a server's status and metrics +func UpdateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) error { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := db.ExecContext(ctx, + `UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5 + WHERE id = $6`, + online, cpuUsage, ramUsage, diskUsage, uptime, serverID, + ) + return err +} + +// CheckAndSendTestNotifications checks for and processes test notifications +func CheckAndSendTestNotifications(db *sql.DB, notifications []models.Notification, sendFunc func(models.Notification, string)) { + // Query for test notifications + rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`) + if err != nil { + fmt.Printf("Error fetching test notifications: %v\n", err) + return + } + defer rows.Close() + + // Process each test notification + var testIds []int + for rows.Next() { + var id, notificationId int + if err := rows.Scan(&id, ¬ificationId); err != nil { + fmt.Printf("Error scanning test notification: %v\n", err) + continue + } + + // Add to list of IDs to delete + testIds = append(testIds, id) + + // Find the notification configuration + for _, n := range notifications { + if n.ID == notificationId { + // Send test notification + fmt.Printf("Sending test notification to notification ID %d\n", notificationId) + sendFunc(n, "Test notification from CoreControl") + } + } + } + + // Delete processed test notifications + if len(testIds) > 0 { + for _, id := range testIds { + _, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id) + if err != nil { + fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err) + } + } + } +} diff --git a/agent/internal/models/models.go b/agent/internal/models/models.go new file mode 100644 index 0000000..d04fa16 --- /dev/null +++ b/agent/internal/models/models.go @@ -0,0 +1,74 @@ +package models + +import ( + "database/sql" +) + +type Application struct { + ID int + Name string + PublicURL string + Online bool +} + +type Server struct { + ID int + Name string + Monitoring bool + MonitoringURL sql.NullString + Online bool + CpuUsage sql.NullFloat64 + RamUsage sql.NullFloat64 + DiskUsage sql.NullFloat64 + Uptime sql.NullString +} + +type CPUResponse struct { + Total float64 `json:"total"` +} + +type MemoryResponse struct { + Active int64 `json:"active"` + Available int64 `json:"available"` + Buffers int64 `json:"buffers"` + Cached int64 `json:"cached"` + Free int64 `json:"free"` + Inactive int64 `json:"inactive"` + Percent float64 `json:"percent"` + Shared int64 `json:"shared"` + Total int64 `json:"total"` + Used int64 `json:"used"` +} + +type FSResponse []struct { + DeviceName string `json:"device_name"` + MntPoint string `json:"mnt_point"` + Percent float64 `json:"percent"` +} + +type UptimeResponse struct { + Value string `json:"value"` +} + +type Notification struct { + ID int + Enabled bool + Type string + SMTPHost sql.NullString + SMTPPort sql.NullInt64 + SMTPFrom sql.NullString + SMTPUser sql.NullString + SMTPPass sql.NullString + SMTPSecure sql.NullBool + SMTPTo sql.NullString + TelegramChatID sql.NullString + TelegramToken sql.NullString + DiscordWebhook sql.NullString + GotifyUrl sql.NullString + GotifyToken sql.NullString + NtfyUrl sql.NullString + NtfyToken sql.NullString + PushoverUrl sql.NullString + PushoverToken sql.NullString + PushoverUser sql.NullString +} diff --git a/agent/internal/notifications/notifications.go b/agent/internal/notifications/notifications.go new file mode 100644 index 0000000..37b459d --- /dev/null +++ b/agent/internal/notifications/notifications.go @@ -0,0 +1,239 @@ +package notifications + +import ( + "fmt" + "net" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/corecontrol/agent/internal/models" + + "gopkg.in/gomail.v2" +) + +type NotificationSender struct { + notifications []models.Notification + notifMutex sync.RWMutex +} + +// NewNotificationSender creates a new notification sender +func NewNotificationSender() *NotificationSender { + return &NotificationSender{ + notifications: []models.Notification{}, + notifMutex: sync.RWMutex{}, + } +} + +// UpdateNotifications updates the stored notifications +func (ns *NotificationSender) UpdateNotifications(notifs []models.Notification) { + ns.notifMutex.Lock() + defer ns.notifMutex.Unlock() + + copyDst := make([]models.Notification, len(notifs)) + copy(copyDst, notifs) + ns.notifications = copyDst +} + +// GetNotifications returns a safe copy of current notifications +func (ns *NotificationSender) GetNotifications() []models.Notification { + ns.notifMutex.RLock() + defer ns.notifMutex.RUnlock() + + copyDst := make([]models.Notification, len(ns.notifications)) + copy(copyDst, ns.notifications) + return copyDst +} + +// SendNotifications sends a message to all configured notifications +func (ns *NotificationSender) SendNotifications(message string) { + notifs := ns.GetNotifications() + + for _, n := range notifs { + ns.SendSpecificNotification(n, message) + } +} + +// SendSpecificNotification sends a message to a specific notification +func (ns *NotificationSender) SendSpecificNotification(n models.Notification, message string) { + fmt.Println("Sending specific notification..." + n.Type) + switch n.Type { + case "smtp": + if n.SMTPHost.Valid && n.SMTPTo.Valid { + ns.sendEmail(n, message) + } + case "telegram": + if n.TelegramToken.Valid && n.TelegramChatID.Valid { + ns.sendTelegram(n, message) + } + case "discord": + if n.DiscordWebhook.Valid { + ns.sendDiscord(n, message) + } + case "gotify": + if n.GotifyUrl.Valid && n.GotifyToken.Valid { + ns.sendGotify(n, message) + } + case "ntfy": + if n.NtfyUrl.Valid && n.NtfyToken.Valid { + ns.sendNtfy(n, message) + } + case "pushover": + if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid { + ns.sendPushover(n, message) + } + } +} + +// Helper function to check if a host is an IP address +func (ns *NotificationSender) isIPAddress(host string) bool { + ip := net.ParseIP(host) + return ip != nil +} + +// Individual notification methods +func (ns *NotificationSender) sendEmail(n models.Notification, body string) { + // Initialize SMTP dialer with host, port, user, pass + d := gomail.NewDialer( + n.SMTPHost.String, + int(n.SMTPPort.Int64), + n.SMTPUser.String, + n.SMTPPass.String, + ) + if n.SMTPSecure.Valid && n.SMTPSecure.Bool { + d.SSL = true + } + + m := gomail.NewMessage() + m.SetHeader("From", n.SMTPFrom.String) + m.SetHeader("To", n.SMTPTo.String) + m.SetHeader("Subject", "Uptime Notification") + m.SetBody("text/plain", body) + + if err := d.DialAndSend(m); err != nil { + fmt.Printf("Email send failed: %v\n", err) + } +} + +func (ns *NotificationSender) sendTelegram(n models.Notification, message string) { + apiUrl := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s", + n.TelegramToken.String, + n.TelegramChatID.String, + message, + ) + resp, err := http.Get(apiUrl) + if err != nil { + fmt.Printf("Telegram send failed: %v\n", err) + return + } + resp.Body.Close() +} + +func (ns *NotificationSender) sendDiscord(n models.Notification, message string) { + payload := fmt.Sprintf(`{"content": "%s"}`, message) + req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload)) + if err != nil { + fmt.Printf("Discord request creation failed: %v\n", err) + return + } + req.Header.Set("Content-Type", "application/json") + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Discord send failed: %v\n", err) + return + } + resp.Body.Close() +} + +func (ns *NotificationSender) sendGotify(n models.Notification, message string) { + baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/") + targetURL := fmt.Sprintf("%s/message", baseURL) + + form := url.Values{} + form.Add("message", message) + form.Add("priority", "5") + + req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode())) + if err != nil { + fmt.Printf("Gotify: ERROR creating request: %v\n", err) + return + } + + req.Header.Set("X-Gotify-Key", n.GotifyToken.String) + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Gotify: ERROR sending request: %v\n", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode) + } +} + +func (ns *NotificationSender) sendNtfy(n models.Notification, message string) { + fmt.Println("Sending Ntfy notification...") + baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/") + + // Don't append a topic to the URL - the URL itself should have the correct endpoint + requestURL := baseURL + + // Send message directly as request body instead of JSON + req, err := http.NewRequest("POST", requestURL, strings.NewReader(message)) + if err != nil { + fmt.Printf("Ntfy: ERROR creating request: %v\n", err) + return + } + + if n.NtfyToken.Valid { + req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String) + } + // Use text/plain instead of application/json + req.Header.Set("Content-Type", "text/plain") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Ntfy: ERROR sending request: %v\n", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode) + } +} + +func (ns *NotificationSender) sendPushover(n models.Notification, message string) { + form := url.Values{} + form.Add("token", n.PushoverToken.String) + form.Add("user", n.PushoverUser.String) + form.Add("message", message) + + req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode())) + if err != nil { + fmt.Printf("Pushover: ERROR creating request: %v\n", err) + return + } + + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + + client := &http.Client{Timeout: 5 * time.Second} + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Pushover: ERROR sending request: %v\n", err) + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode) + } +} diff --git a/agent/internal/server/monitor.go b/agent/internal/server/monitor.go new file mode 100644 index 0000000..45d2acc --- /dev/null +++ b/agent/internal/server/monitor.go @@ -0,0 +1,291 @@ +package server + +import ( + "context" + "database/sql" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + "time" + + "github.com/corecontrol/agent/internal/models" + "github.com/corecontrol/agent/internal/notifications" +) + +// MonitorServers checks and updates the status of all servers +func MonitorServers(db *sql.DB, client *http.Client, servers []models.Server, notifSender *notifications.NotificationSender) { + var notificationTemplate string + err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate) + if err != nil || notificationTemplate == "" { + notificationTemplate = "The server !name is now !status!" + } + + for _, server := range servers { + if !server.Monitoring || !server.MonitoringURL.Valid { + continue + } + + logPrefix := fmt.Sprintf("[Server %s]", server.Name) + fmt.Printf("%s Checking...\n", logPrefix) + + baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/") + var cpuUsage, ramUsage, diskUsage float64 + var online = true + var uptimeStr string + + // Get CPU usage + online, cpuUsage = fetchCPUUsage(client, baseURL, logPrefix) + if !online { + updateServerStatus(db, server.ID, false, 0, 0, 0, "") + sendStatusChangeNotification(server, online, notificationTemplate, notifSender) + addServerHistoryEntry(db, server.ID, false, 0, 0, 0) + continue + } + + // Get uptime if server is online + uptimeStr = fetchUptime(client, baseURL, logPrefix) + + // Get Memory usage + memOnline, memUsage := fetchMemoryUsage(client, baseURL, logPrefix) + if !memOnline { + online = false + updateServerStatus(db, server.ID, false, 0, 0, 0, "") + sendStatusChangeNotification(server, online, notificationTemplate, notifSender) + addServerHistoryEntry(db, server.ID, false, 0, 0, 0) + continue + } + ramUsage = memUsage + + // Get Disk usage + diskOnline, diskUsageVal := fetchDiskUsage(client, baseURL, logPrefix) + if !diskOnline { + online = false + updateServerStatus(db, server.ID, false, 0, 0, 0, "") + sendStatusChangeNotification(server, online, notificationTemplate, notifSender) + addServerHistoryEntry(db, server.ID, false, 0, 0, 0) + continue + } + diskUsage = diskUsageVal + + // Check if status changed and send notification if needed + if online != server.Online { + sendStatusChangeNotification(server, online, notificationTemplate, notifSender) + } + + // Update server status with metrics + updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr) + + // Add entry to server history + addServerHistoryEntry(db, server.ID, online, cpuUsage, ramUsage, diskUsage) + + fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n", + logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr) + } +} + +// Helper function to fetch CPU usage +func fetchCPUUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { + cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL)) + if err != nil { + fmt.Printf("%s CPU request failed: %v\n", logPrefix, err) + return false, 0 + } + defer cpuResp.Body.Close() + + if cpuResp.StatusCode != http.StatusOK { + fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode) + return false, 0 + } + + var cpuData models.CPUResponse + if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil { + fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err) + return false, 0 + } + + return true, cpuData.Total +} + +// Helper function to fetch memory usage +func fetchMemoryUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { + memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL)) + if err != nil { + fmt.Printf("%s Memory request failed: %v\n", logPrefix, err) + return false, 0 + } + defer memResp.Body.Close() + + if memResp.StatusCode != http.StatusOK { + fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode) + return false, 0 + } + + var memData models.MemoryResponse + if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil { + fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err) + return false, 0 + } + + return true, memData.Percent +} + +// Helper function to fetch disk usage +func fetchDiskUsage(client *http.Client, baseURL, logPrefix string) (bool, float64) { + fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL)) + if err != nil { + fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err) + return false, 0 + } + defer fsResp.Body.Close() + + if fsResp.StatusCode != http.StatusOK { + fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode) + return false, 0 + } + + var fsData models.FSResponse + if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil { + fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err) + return false, 0 + } + + if len(fsData) > 0 { + return true, fsData[0].Percent + } + + return true, 0 +} + +// Helper function to fetch uptime +func fetchUptime(client *http.Client, baseURL, logPrefix string) string { + uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL)) + if err != nil || uptimeResp.StatusCode != http.StatusOK { + if err != nil { + fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err) + } else { + fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode) + uptimeResp.Body.Close() + } + return "" + } + defer uptimeResp.Body.Close() + + // Read the response body as a string first + uptimeBytes, err := io.ReadAll(uptimeResp.Body) + if err != nil { + fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err) + return "" + } + + uptimeStr := strings.Trim(string(uptimeBytes), "\"") + + // Try to parse as JSON object first, then fallback to direct string if that fails + var uptimeData models.UptimeResponse + if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" { + uptimeStr = formatUptime(uptimeData.Value) + } else { + // Use the string directly + uptimeStr = formatUptime(uptimeStr) + } + + fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr) + return uptimeStr +} + +// Helper function to send notification about status change +func sendStatusChangeNotification(server models.Server, online bool, template string, notifSender *notifications.NotificationSender) { + status := "offline" + if online { + status = "online" + } + + message := strings.ReplaceAll(template, "!name", server.Name) + message = strings.ReplaceAll(message, "!status", status) + + notifSender.SendNotifications(message) +} + +// Helper function to update server status +func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := db.ExecContext(ctx, + `UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5 + WHERE id = $6`, + online, cpuUsage, ramUsage, diskUsage, uptime, serverID, + ) + if err != nil { + fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err) + } +} + +// Helper function to add server history entry +func addServerHistoryEntry(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + _, err := db.ExecContext(ctx, + `INSERT INTO server_history( + "serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt" + ) VALUES ($1, $2, $3, $4, $5, now())`, + serverID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage), + ) + if err != nil { + fmt.Printf("Failed to insert server history (ID: %d): %v\n", serverID, err) + } +} + +// FormatUptime formats the uptime string to a standard format +func formatUptime(uptimeStr string) string { + // Example input: "3 days, 3:52:36" + // Target output: "28.6 13:52" + + now := time.Now() + + // Parse the uptime components + parts := strings.Split(uptimeStr, ", ") + + var days int + var timeStr string + + if len(parts) == 2 { + // Has days part and time part + _, err := fmt.Sscanf(parts[0], "%d days", &days) + if err != nil { + // Try singular "day" + _, err = fmt.Sscanf(parts[0], "%d day", &days) + if err != nil { + return uptimeStr // Return original if parsing fails + } + } + timeStr = parts[1] + } else if len(parts) == 1 { + // Only has time part (less than a day) + days = 0 + timeStr = parts[0] + } else { + return uptimeStr // Return original if format is unexpected + } + + // Parse the time component (hours:minutes:seconds) + var hours, minutes, seconds int + _, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds) + if err != nil { + return uptimeStr // Return original if parsing fails + } + + // Calculate the total duration + duration := time.Duration(days)*24*time.Hour + + time.Duration(hours)*time.Hour + + time.Duration(minutes)*time.Minute + + time.Duration(seconds)*time.Second + + // Calculate the start time by subtracting the duration from now + startTime := now.Add(-duration) + + // Format the result in the required format (day.month hour:minute) + return startTime.Format("2.1 15:04") +} diff --git a/agent/main.go b/agent/main.go deleted file mode 100644 index 0a858d8..0000000 --- a/agent/main.go +++ /dev/null @@ -1,878 +0,0 @@ -package main - -import ( - "context" - "crypto/x509" - "database/sql" - "encoding/json" - "errors" - "fmt" - "io" - "net" - "net/http" - "net/url" - "os" - "strings" - "sync" - "time" - - _ "github.com/jackc/pgx/v4/stdlib" - "github.com/joho/godotenv" - "gopkg.in/gomail.v2" -) - -type Application struct { - ID int - Name string - PublicURL string - Online bool -} - -type Server struct { - ID int - Name string - Monitoring bool - MonitoringURL sql.NullString - Online bool - CpuUsage sql.NullFloat64 - RamUsage sql.NullFloat64 - DiskUsage sql.NullFloat64 - Uptime sql.NullString -} - -type CPUResponse struct { - Total float64 `json:"total"` -} - -type MemoryResponse struct { - Active int64 `json:"active"` - Available int64 `json:"available"` - Buffers int64 `json:"buffers"` - Cached int64 `json:"cached"` - Free int64 `json:"free"` - Inactive int64 `json:"inactive"` - Percent float64 `json:"percent"` - Shared int64 `json:"shared"` - Total int64 `json:"total"` - Used int64 `json:"used"` -} - -type FSResponse []struct { - DeviceName string `json:"device_name"` - MntPoint string `json:"mnt_point"` - Percent float64 `json:"percent"` -} - -type UptimeResponse struct { - Value string `json:"value"` -} - -type Notification struct { - ID int - Enabled bool - Type string - SMTPHost sql.NullString - SMTPPort sql.NullInt64 - SMTPFrom sql.NullString - SMTPUser sql.NullString - SMTPPass sql.NullString - SMTPSecure sql.NullBool - SMTPTo sql.NullString - TelegramChatID sql.NullString - TelegramToken sql.NullString - DiscordWebhook sql.NullString - GotifyUrl sql.NullString - GotifyToken sql.NullString - NtfyUrl sql.NullString - NtfyToken sql.NullString - PushoverUrl sql.NullString - PushoverToken sql.NullString - PushoverUser sql.NullString -} - -var ( - notifications []Notification - notifMutex sync.RWMutex -) - -func main() { - if err := godotenv.Load(); err != nil { - fmt.Println("No env vars found") - } - - dbURL := os.Getenv("DATABASE_URL") - if dbURL == "" { - panic("DATABASE_URL not set") - } - - db, err := sql.Open("pgx", dbURL) - if err != nil { - panic(fmt.Sprintf("Database connection failed: %v\n", err)) - } - defer db.Close() - - // initial load - notifs, err := loadNotifications(db) - if err != nil { - panic(fmt.Sprintf("Failed to load notifications: %v", err)) - } - notifMutex.Lock() - notifications = notifMutexCopy(notifs) - notifMutex.Unlock() - - // reload notification configs every minute - go func() { - reloadTicker := time.NewTicker(time.Minute) - defer reloadTicker.Stop() - - for range reloadTicker.C { - newNotifs, err := loadNotifications(db) - if err != nil { - fmt.Printf("Failed to reload notifications: %v\n", err) - continue - } - notifMutex.Lock() - notifications = notifMutexCopy(newNotifs) - notifMutex.Unlock() - fmt.Println("Reloaded notification configurations") - } - }() - - // clean up old entries hourly - go func() { - deletionTicker := time.NewTicker(time.Hour) - defer deletionTicker.Stop() - - for range deletionTicker.C { - if err := deleteOldEntries(db); err != nil { - fmt.Printf("Error deleting old entries: %v\n", err) - } - } - }() - - // Check for test notifications every 10 seconds - go func() { - testNotifTicker := time.NewTicker(10 * time.Second) - defer testNotifTicker.Stop() - - for range testNotifTicker.C { - checkAndSendTestNotifications(db) - } - }() - - appClient := &http.Client{ - Timeout: 4 * time.Second, - } - - // Server monitoring every 5 seconds - go func() { - serverClient := &http.Client{ - Timeout: 5 * time.Second, - } - serverTicker := time.NewTicker(5 * time.Second) - defer serverTicker.Stop() - - for range serverTicker.C { - servers := getServers(db) - checkAndUpdateServerStatus(db, serverClient, servers) - } - }() - - ticker := time.NewTicker(time.Second) - defer ticker.Stop() - - for now := range ticker.C { - if now.Second()%10 != 0 { - continue - } - - apps := getApplications(db) - checkAndUpdateStatus(db, appClient, apps) - } -} - -// helper to safely copy slice -func notifMutexCopy(src []Notification) []Notification { - copyDst := make([]Notification, len(src)) - copy(copyDst, src) - return copyDst -} - -func isIPAddress(host string) bool { - ip := net.ParseIP(host) - return ip != nil -} - -func loadNotifications(db *sql.DB) ([]Notification, error) { - rows, err := db.Query( - `SELECT id, enabled, type, "smtpHost", "smtpPort", "smtpFrom", "smtpUser", "smtpPass", "smtpSecure", "smtpTo", - "telegramChatId", "telegramToken", "discordWebhook", "gotifyUrl", "gotifyToken", "ntfyUrl", "ntfyToken" - FROM notification - WHERE enabled = true`, - ) - if err != nil { - return nil, err - } - defer rows.Close() - - var configs []Notification - for rows.Next() { - var n Notification - if err := rows.Scan( - &n.ID, &n.Enabled, &n.Type, - &n.SMTPHost, &n.SMTPPort, &n.SMTPFrom, &n.SMTPUser, &n.SMTPPass, &n.SMTPSecure, &n.SMTPTo, - &n.TelegramChatID, &n.TelegramToken, &n.DiscordWebhook, &n.GotifyUrl, &n.GotifyToken, &n.NtfyUrl, &n.NtfyToken, - ); err != nil { - fmt.Printf("Error scanning notification: %v\n", err) - continue - } - configs = append(configs, n) - } - return configs, nil -} - -func deleteOldEntries(db *sql.DB) error { - ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) - defer cancel() - - // Delete old uptime history entries - res, err := db.ExecContext(ctx, - `DELETE FROM uptime_history WHERE "createdAt" < now() - interval '30 days'`, - ) - if err != nil { - return err - } - affected, _ := res.RowsAffected() - fmt.Printf("Deleted %d old entries from uptime_history\n", affected) - - // Delete old server history entries - res, err = db.ExecContext(ctx, - `DELETE FROM server_history WHERE "createdAt" < now() - interval '30 days'`, - ) - if err != nil { - return err - } - affected, _ = res.RowsAffected() - fmt.Printf("Deleted %d old entries from server_history\n", affected) - - return nil -} - -func getApplications(db *sql.DB) []Application { - rows, err := db.Query( - `SELECT id, name, "publicURL", online FROM application WHERE "publicURL" IS NOT NULL`, - ) - if err != nil { - fmt.Printf("Error fetching applications: %v\n", err) - return nil - } - defer rows.Close() - - var apps []Application - for rows.Next() { - var app Application - if err := rows.Scan(&app.ID, &app.Name, &app.PublicURL, &app.Online); err != nil { - fmt.Printf("Error scanning row: %v\n", err) - continue - } - apps = append(apps, app) - } - return apps -} - -func getServers(db *sql.DB) []Server { - rows, err := db.Query( - `SELECT id, name, monitoring, "monitoringURL", online, "cpuUsage", "ramUsage", "diskUsage" - FROM server WHERE monitoring = true`, - ) - if err != nil { - fmt.Printf("Error fetching servers: %v\n", err) - return nil - } - defer rows.Close() - - var servers []Server - for rows.Next() { - var server Server - if err := rows.Scan( - &server.ID, &server.Name, &server.Monitoring, &server.MonitoringURL, - &server.Online, &server.CpuUsage, &server.RamUsage, &server.DiskUsage, - ); err != nil { - fmt.Printf("Error scanning server row: %v\n", err) - continue - } - servers = append(servers, server) - } - return servers -} - -func checkAndUpdateStatus(db *sql.DB, client *http.Client, apps []Application) { - var notificationTemplate string - err := db.QueryRow("SELECT notification_text_application FROM settings LIMIT 1").Scan(¬ificationTemplate) - if err != nil || notificationTemplate == "" { - notificationTemplate = "The application !name (!url) went !status!" - } - - for _, app := range apps { - logPrefix := fmt.Sprintf("[App %s (%s)]", app.Name, app.PublicURL) - fmt.Printf("%s Checking...\n", logPrefix) - - parsedURL, parseErr := url.Parse(app.PublicURL) - if parseErr != nil { - fmt.Printf("%s Invalid URL: %v\n", logPrefix, parseErr) - continue - } - - hostIsIP := isIPAddress(parsedURL.Hostname()) - var isOnline bool - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - req, err := http.NewRequestWithContext(ctx, "GET", app.PublicURL, nil) - if err != nil { - fmt.Printf("%s Request creation failed: %v\n", logPrefix, err) - continue - } - - resp, err := client.Do(req) - if err == nil { - defer resp.Body.Close() - isOnline = resp.StatusCode >= 200 && resp.StatusCode < 400 - fmt.Printf("%s Response status: %d\n", logPrefix, resp.StatusCode) - } else { - fmt.Printf("%s Connection error: %v\n", logPrefix, err) - - if hostIsIP { - var urlErr *url.Error - if errors.As(err, &urlErr) { - var certErr x509.HostnameError - var unknownAuthErr x509.UnknownAuthorityError - if errors.As(urlErr.Err, &certErr) || errors.As(urlErr.Err, &unknownAuthErr) { - fmt.Printf("%s Ignoring TLS error for IP, marking as online\n", logPrefix) - isOnline = true - } - } - } - } - - if isOnline != app.Online { - status := "offline" - if isOnline { - status = "online" - } - - message := strings.ReplaceAll(notificationTemplate, "!name", app.Name) - message = strings.ReplaceAll(message, "!url", app.PublicURL) - message = strings.ReplaceAll(message, "!status", status) - - sendNotifications(message) - } - - dbCtx, dbCancel := context.WithTimeout(context.Background(), 5*time.Second) - _, err = db.ExecContext(dbCtx, - `UPDATE application SET online = $1 WHERE id = $2`, - isOnline, app.ID, - ) - if err != nil { - fmt.Printf("%s DB update failed: %v\n", logPrefix, err) - } - dbCancel() - - dbCtx2, dbCancel2 := context.WithTimeout(context.Background(), 5*time.Second) - _, err = db.ExecContext(dbCtx2, - `INSERT INTO uptime_history("applicationId", online, "createdAt") VALUES ($1, $2, now())`, - app.ID, isOnline, - ) - if err != nil { - fmt.Printf("%s History insert failed: %v\n", logPrefix, err) - } - dbCancel2() - } -} - -func checkAndUpdateServerStatus(db *sql.DB, client *http.Client, servers []Server) { - var notificationTemplate string - err := db.QueryRow("SELECT notification_text_server FROM settings LIMIT 1").Scan(¬ificationTemplate) - if err != nil || notificationTemplate == "" { - notificationTemplate = "The server !name is now !status!" - } - - for _, server := range servers { - if !server.Monitoring || !server.MonitoringURL.Valid { - continue - } - - logPrefix := fmt.Sprintf("[Server %s]", server.Name) - fmt.Printf("%s Checking...\n", logPrefix) - - baseURL := strings.TrimSuffix(server.MonitoringURL.String, "/") - var cpuUsage, ramUsage, diskUsage float64 - var online = true - var uptimeStr string - - // Get CPU usage - cpuResp, err := client.Get(fmt.Sprintf("%s/api/4/cpu", baseURL)) - if err != nil { - fmt.Printf("%s CPU request failed: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - defer cpuResp.Body.Close() - - if cpuResp.StatusCode != http.StatusOK { - fmt.Printf("%s Bad CPU status code: %d\n", logPrefix, cpuResp.StatusCode) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - var cpuData CPUResponse - if err := json.NewDecoder(cpuResp.Body).Decode(&cpuData); err != nil { - fmt.Printf("%s Failed to parse CPU JSON: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - cpuUsage = cpuData.Total - } - } - } - - // Get uptime if server is online - if online { - uptimeResp, err := client.Get(fmt.Sprintf("%s/api/4/uptime", baseURL)) - if err == nil && uptimeResp.StatusCode == http.StatusOK { - defer uptimeResp.Body.Close() - - // Read the response body as a string first - uptimeBytes, err := io.ReadAll(uptimeResp.Body) - if err == nil { - uptimeStr = strings.Trim(string(uptimeBytes), "\"") - - // Try to parse as JSON object first, then fallback to direct string if that fails - var uptimeData UptimeResponse - if jsonErr := json.Unmarshal(uptimeBytes, &uptimeData); jsonErr == nil && uptimeData.Value != "" { - uptimeStr = formatUptime(uptimeData.Value) - } else { - // Use the string directly - uptimeStr = formatUptime(uptimeStr) - } - - fmt.Printf("%s Uptime: %s (formatted: %s)\n", logPrefix, string(uptimeBytes), uptimeStr) - } else { - fmt.Printf("%s Failed to read uptime response: %v\n", logPrefix, err) - } - } else { - if err != nil { - fmt.Printf("%s Uptime request failed: %v\n", logPrefix, err) - } else { - fmt.Printf("%s Bad uptime status code: %d\n", logPrefix, uptimeResp.StatusCode) - uptimeResp.Body.Close() - } - } - } - - if online { - // Get Memory usage - memResp, err := client.Get(fmt.Sprintf("%s/api/4/mem", baseURL)) - if err != nil { - fmt.Printf("%s Memory request failed: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - defer memResp.Body.Close() - - if memResp.StatusCode != http.StatusOK { - fmt.Printf("%s Bad memory status code: %d\n", logPrefix, memResp.StatusCode) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - var memData MemoryResponse - if err := json.NewDecoder(memResp.Body).Decode(&memData); err != nil { - fmt.Printf("%s Failed to parse memory JSON: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - ramUsage = memData.Percent - } - } - } - } - - if online { - // Get Disk usage - fsResp, err := client.Get(fmt.Sprintf("%s/api/4/fs", baseURL)) - if err != nil { - fmt.Printf("%s Filesystem request failed: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - defer fsResp.Body.Close() - - if fsResp.StatusCode != http.StatusOK { - fmt.Printf("%s Bad filesystem status code: %d\n", logPrefix, fsResp.StatusCode) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else { - var fsData FSResponse - if err := json.NewDecoder(fsResp.Body).Decode(&fsData); err != nil { - fmt.Printf("%s Failed to parse filesystem JSON: %v\n", logPrefix, err) - updateServerStatus(db, server.ID, false, 0, 0, 0, "") - online = false - } else if len(fsData) > 0 { - diskUsage = fsData[0].Percent - } - } - } - } - - // Check if status changed and send notification if needed - if online != server.Online { - status := "offline" - if online { - status = "online" - } - - message := notificationTemplate - message = strings.ReplaceAll(message, "!name", server.Name) - message = strings.ReplaceAll(message, "!status", status) - - sendNotifications(message) - } - - // Update server status with metrics - updateServerStatus(db, server.ID, online, cpuUsage, ramUsage, diskUsage, uptimeStr) - - // Add entry to server history - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - _, err = db.ExecContext(ctx, - `INSERT INTO server_history( - "serverId", online, "cpuUsage", "ramUsage", "diskUsage", "createdAt" - ) VALUES ($1, $2, $3, $4, $5, now())`, - server.ID, online, fmt.Sprintf("%.2f", cpuUsage), fmt.Sprintf("%.2f", ramUsage), fmt.Sprintf("%.2f", diskUsage), - ) - cancel() - if err != nil { - fmt.Printf("%s Failed to insert history: %v\n", logPrefix, err) - } - - fmt.Printf("%s Updated - CPU: %.2f%%, RAM: %.2f%%, Disk: %.2f%%, Uptime: %s\n", - logPrefix, cpuUsage, ramUsage, diskUsage, uptimeStr) - } -} - -func formatUptime(uptimeStr string) string { - // Example input: "3 days, 3:52:36" - // Target output: "28.6 13:52" - - now := time.Now() - - // Parse the uptime components - parts := strings.Split(uptimeStr, ", ") - - var days int - var timeStr string - - if len(parts) == 2 { - // Has days part and time part - _, err := fmt.Sscanf(parts[0], "%d days", &days) - if err != nil { - // Try singular "day" - _, err = fmt.Sscanf(parts[0], "%d day", &days) - if err != nil { - return uptimeStr // Return original if parsing fails - } - } - timeStr = parts[1] - } else if len(parts) == 1 { - // Only has time part (less than a day) - days = 0 - timeStr = parts[0] - } else { - return uptimeStr // Return original if format is unexpected - } - - // Parse the time component (hours:minutes:seconds) - var hours, minutes, seconds int - _, err := fmt.Sscanf(timeStr, "%d:%d:%d", &hours, &minutes, &seconds) - if err != nil { - return uptimeStr // Return original if parsing fails - } - - // Calculate the total duration - duration := time.Duration(days)*24*time.Hour + - time.Duration(hours)*time.Hour + - time.Duration(minutes)*time.Minute + - time.Duration(seconds)*time.Second - - // Calculate the start time by subtracting the duration from now - startTime := now.Add(-duration) - - // Format the result in the required format (day.month hour:minute) - return startTime.Format("2.1 15:04") -} - -func updateServerStatus(db *sql.DB, serverID int, online bool, cpuUsage, ramUsage, diskUsage float64, uptime string) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - _, err := db.ExecContext(ctx, - `UPDATE server SET online = $1, "cpuUsage" = $2::float8, "ramUsage" = $3::float8, "diskUsage" = $4::float8, "uptime" = $5 - WHERE id = $6`, - online, cpuUsage, ramUsage, diskUsage, uptime, serverID, - ) - if err != nil { - fmt.Printf("Failed to update server status (ID: %d): %v\n", serverID, err) - } -} - -func sendNotifications(message string) { - notifMutex.RLock() - notifs := notifMutexCopy(notifications) - notifMutex.RUnlock() - - for _, n := range notifs { - switch n.Type { - case "smtp": - if n.SMTPHost.Valid && n.SMTPTo.Valid { - sendEmail(n, message) - } - case "telegram": - if n.TelegramToken.Valid && n.TelegramChatID.Valid { - sendTelegram(n, message) - } - case "discord": - if n.DiscordWebhook.Valid { - sendDiscord(n, message) - } - case "gotify": - if n.GotifyUrl.Valid && n.GotifyToken.Valid { - sendGotify(n, message) - } - case "ntfy": - if n.NtfyUrl.Valid && n.NtfyToken.Valid { - sendNtfy(n, message) - } - case "pushover": - if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid { - sendPushover(n, message) - } - } - } -} - -func sendEmail(n Notification, body string) { - // Initialize SMTP dialer with host, port, user, pass - d := gomail.NewDialer( - n.SMTPHost.String, - int(n.SMTPPort.Int64), - n.SMTPUser.String, - n.SMTPPass.String, - ) - if n.SMTPSecure.Valid && n.SMTPSecure.Bool { - d.SSL = true - } - - m := gomail.NewMessage() - m.SetHeader("From", n.SMTPFrom.String) - m.SetHeader("To", n.SMTPTo.String) - m.SetHeader("Subject", "Uptime Notification") - m.SetBody("text/plain", body) - - if err := d.DialAndSend(m); err != nil { - fmt.Printf("Email send failed: %v\n", err) - } -} - -func sendTelegram(n Notification, message string) { - url := fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage?chat_id=%s&text=%s", - n.TelegramToken.String, - n.TelegramChatID.String, - message, - ) - resp, err := http.Get(url) - if err != nil { - fmt.Printf("Telegram send failed: %v\n", err) - return - } - resp.Body.Close() -} - -func sendDiscord(n Notification, message string) { - payload := fmt.Sprintf(`{"content": "%s"}`, message) - req, err := http.NewRequest("POST", n.DiscordWebhook.String, strings.NewReader(payload)) - if err != nil { - fmt.Printf("Discord request creation failed: %v\n", err) - return - } - req.Header.Set("Content-Type", "application/json") - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Discord send failed: %v\n", err) - return - } - resp.Body.Close() -} - -func sendGotify(n Notification, message string) { - baseURL := strings.TrimSuffix(n.GotifyUrl.String, "/") - targetURL := fmt.Sprintf("%s/message", baseURL) - - form := url.Values{} - form.Add("message", message) - form.Add("priority", "5") - - req, err := http.NewRequest("POST", targetURL, strings.NewReader(form.Encode())) - if err != nil { - fmt.Printf("Gotify: ERROR creating request: %v\n", err) - return - } - - req.Header.Set("X-Gotify-Key", n.GotifyToken.String) - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Gotify: ERROR sending request: %v\n", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - fmt.Printf("Gotify: ERROR status code: %d\n", resp.StatusCode) - } -} - -func sendNtfy(n Notification, message string) { - fmt.Println("Sending Ntfy notification...") - baseURL := strings.TrimSuffix(n.NtfyUrl.String, "/") - - // Don't append a topic to the URL - the URL itself should have the correct endpoint - requestURL := baseURL - - // Send message directly as request body instead of JSON - req, err := http.NewRequest("POST", requestURL, strings.NewReader(message)) - if err != nil { - fmt.Printf("Ntfy: ERROR creating request: %v\n", err) - return - } - - if n.NtfyToken.Valid { - req.Header.Set("Authorization", "Bearer "+n.NtfyToken.String) - } - // Use text/plain instead of application/json - req.Header.Set("Content-Type", "text/plain") - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Ntfy: ERROR sending request: %v\n", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - fmt.Printf("Ntfy: ERROR status code: %d\n", resp.StatusCode) - } -} - -func sendPushover(n Notification, message string) { - form := url.Values{} - form.Add("token", n.PushoverToken.String) - form.Add("user", n.PushoverUser.String) - form.Add("message", message) - - req, err := http.NewRequest("POST", n.PushoverUrl.String, strings.NewReader(form.Encode())) - if err != nil { - fmt.Printf("Pushover: ERROR creating request: %v\n", err) - return - } - - req.Header.Set("Content-Type", "application/x-www-form-urlencoded") - - client := &http.Client{Timeout: 5 * time.Second} - resp, err := client.Do(req) - if err != nil { - fmt.Printf("Pushover: ERROR sending request: %v\n", err) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - fmt.Printf("Pushover: ERROR status code: %d\n", resp.StatusCode) - } -} - -func checkAndSendTestNotifications(db *sql.DB) { - // Query for test notifications - rows, err := db.Query(`SELECT tn.id, tn."notificationId" FROM test_notification tn`) - if err != nil { - fmt.Printf("Error fetching test notifications: %v\n", err) - return - } - defer rows.Close() - - // Process each test notification - var testIds []int - for rows.Next() { - var id, notificationId int - if err := rows.Scan(&id, ¬ificationId); err != nil { - fmt.Printf("Error scanning test notification: %v\n", err) - continue - } - - // Add to list of IDs to delete - testIds = append(testIds, id) - - // Find the notification configuration - notifMutex.RLock() - for _, n := range notifications { - if n.ID == notificationId { - // Send test notification - fmt.Printf("Sending test notification to notification ID %d\n", notificationId) - sendSpecificNotification(n, "Test notification from CoreControl") - } - } - notifMutex.RUnlock() - } - - // Delete processed test notifications - if len(testIds) > 0 { - for _, id := range testIds { - _, err := db.Exec(`DELETE FROM test_notification WHERE id = $1`, id) - if err != nil { - fmt.Printf("Error deleting test notification (ID: %d): %v\n", id, err) - } - } - } -} - -func sendSpecificNotification(n Notification, message string) { - fmt.Println("Sending specific notification..." + n.Type) - switch n.Type { - case "smtp": - if n.SMTPHost.Valid && n.SMTPTo.Valid { - sendEmail(n, message) - } - case "telegram": - if n.TelegramToken.Valid && n.TelegramChatID.Valid { - sendTelegram(n, message) - } - case "discord": - if n.DiscordWebhook.Valid { - sendDiscord(n, message) - } - case "gotify": - if n.GotifyUrl.Valid && n.GotifyToken.Valid { - sendGotify(n, message) - } - case "ntfy": - if n.NtfyUrl.Valid && n.NtfyToken.Valid { - sendNtfy(n, message) - } - case "pushover": - if n.PushoverUrl.Valid && n.PushoverToken.Valid && n.PushoverUser.Valid { - sendPushover(n, message) - } - } -}