mirror of
https://github.com/crocofied/CoreControl.git
synced 2025-12-18 16:07:10 +00:00
Split go agent into multiple files for better overview
This commit is contained in:
parent
00a628031e
commit
78c844b1fe
111
agent/cmd/agent/main.go
Normal file
111
agent/cmd/agent/main.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
14
agent/go.mod
14
agent/go.mod
@ -1,22 +1,22 @@
|
|||||||
module agent
|
module github.com/corecontrol/agent
|
||||||
|
|
||||||
go 1.24.1
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/pgx/v4 v4.18.3
|
github.com/jackc/pgx/v4 v4.18.1
|
||||||
github.com/joho/godotenv v1.5.1
|
github.com/joho/godotenv v1.5.1
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
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/pgio v1.0.0 // indirect
|
||||||
github.com/jackc/pgpassfile 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/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
github.com/jackc/pgtype v1.14.0 // indirect
|
github.com/jackc/pgtype v1.14.0 // indirect
|
||||||
golang.org/x/crypto v0.31.0 // indirect
|
golang.org/x/crypto v0.6.0 // indirect
|
||||||
golang.org/x/text v0.21.0 // indirect
|
golang.org/x/text v0.7.0 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
39
agent/go.sum
39
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.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.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.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.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
|
||||||
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
|
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 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
|
||||||
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
|
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=
|
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.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.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.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.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
|
||||||
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
|
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-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 h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
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-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.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.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.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
|
||||||
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
|
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-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 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.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 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
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=
|
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.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.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
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.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.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.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 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
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=
|
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.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=
|
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-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-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.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.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
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/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.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.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-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-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-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-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-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-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-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-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/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-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-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-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-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-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.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.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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.4/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.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
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-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-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
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-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-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-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.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-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-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
123
agent/internal/app/monitor.go
Normal file
123
agent/internal/app/monitor.go
Normal file
@ -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
|
||||||
|
}
|
||||||
198
agent/internal/database/database.go
Normal file
198
agent/internal/database/database.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
74
agent/internal/models/models.go
Normal file
74
agent/internal/models/models.go
Normal file
@ -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
|
||||||
|
}
|
||||||
239
agent/internal/notifications/notifications.go
Normal file
239
agent/internal/notifications/notifications.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
291
agent/internal/server/monitor.go
Normal file
291
agent/internal/server/monitor.go
Normal file
@ -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")
|
||||||
|
}
|
||||||
878
agent/main.go
878
agent/main.go
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user