Split go agent into multiple files for better overview

This commit is contained in:
headlessdev 2025-04-26 16:11:06 +02:00
parent 00a628031e
commit 78c844b1fe
9 changed files with 1072 additions and 895 deletions

111
agent/cmd/agent/main.go Normal file
View 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)
}
}

View File

@ -1,22 +1,22 @@
module agent
module github.com/corecontrol/agent
go 1.24.1
go 1.19
require (
github.com/jackc/pgx/v4 v4.18.3
github.com/jackc/pgx/v4 v4.18.1
github.com/joho/godotenv v1.5.1
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
require (
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.14.3 // indirect
github.com/jackc/pgconn v1.14.0 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.3.3 // indirect
github.com/jackc/pgproto3/v2 v2.3.2 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgtype v1.14.0 // indirect
golang.org/x/crypto v0.31.0 // indirect
golang.org/x/text v0.21.0 // indirect
golang.org/x/crypto v0.6.0 // indirect
golang.org/x/text v0.7.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
)

View File

@ -25,8 +25,8 @@ github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsU
github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o=
github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY=
github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI=
github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w=
github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM=
github.com/jackc/pgconn v1.14.0 h1:vrbA9Ud87g6JdFWkHTJXppVce58qPIdP7N8y0Ml/A7Q=
github.com/jackc/pgconn v1.14.0/go.mod h1:9mBNlny0UvkgJdCDvdVHYSjI+8tD2rnKK69Wz8ti++E=
github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE=
github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8=
github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE=
@ -42,8 +42,8 @@ github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvW
github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM=
github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag=
github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgproto3/v2 v2.3.2 h1:7eY55bdBeCz1F2fTzSz69QC+pG46jYq9/jtSPiJ5nn0=
github.com/jackc/pgproto3/v2 v2.3.2/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
@ -57,11 +57,12 @@ github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08
github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM=
github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc=
github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs=
github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA=
github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw=
github.com/jackc/pgx/v4 v4.18.1 h1:YP7G1KABtKpB5IHrO9vYwSrCOhs7p3uqhvhhQBptya0=
github.com/jackc/pgx/v4 v4.18.1/go.mod h1:FydWkUyadDmdNH/mHnGob881GawxeEm7TcMCzkb+qQE=
github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@ -98,13 +99,18 @@ github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6Mwd
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@ -126,17 +132,22 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -149,15 +160,21 @@ golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@ -165,7 +182,9 @@ golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgw
golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View 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(&notificationTemplate)
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
}

View 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, &notificationId); 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)
}
}
}
}

View 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
}

View 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)
}
}

View 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(&notificationTemplate)
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")
}

View File

@ -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(&notificationTemplate)
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(&notificationTemplate)
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, &notificationId); 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)
}
}
}