kiss server monitoring tool with email alerts
go monitoring

feat: init servmon

+15
.github/workflows/build.yml
···
+
name: Build
+
on:
+
push:
+
branches:
+
- main
+
pull_request:
+
jobs:
+
build:
+
runs-on: ubuntu-latest
+
steps:
+
- uses: actions/checkout@v4
+
- uses: actions/setup-go@v5
+
with:
+
go-version: "stable"
+
- run: make build
+15
.github/workflows/test.yml
···
+
name: Test
+
on:
+
push:
+
branches:
+
- main
+
pull_request:
+
jobs:
+
test:
+
runs-on: ubuntu-latest
+
steps:
+
- uses: actions/checkout@v4
+
- uses: actions/setup-go@v5
+
with:
+
go-version: "stable"
+
- run: go test -cover ./...
+27
.gitignore
···
+
# If you prefer the allow list template instead of the deny list, see community template:
+
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+
#
+
# Binaries for programs and plugins
+
*.exe
+
*.exe~
+
*.dll
+
*.so
+
*.dylib
+
servmon-bin
+
+
# Test binary, built with `go test -c`
+
*.test
+
+
# Output of the go coverage tool, specifically when used with LiteIDE
+
*.out
+
+
# Dependency directories (remove the comment below to include it)
+
# vendor/
+
+
# Go workspace file
+
go.work
+
go.work.sum
+
+
# env file
+
.env
+
.servmon.yaml
+21
LICENSE
···
+
MIT License
+
+
Copyright (c) 2025 Julien Robert
+
+
Permission is hereby granted, free of charge, to any person obtaining a copy
+
of this software and associated documentation files (the "Software"), to deal
+
in the Software without restriction, including without limitation the rights
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+
copies of the Software, and to permit persons to whom the Software is
+
furnished to do so, subject to the following conditions:
+
+
The above copyright notice and this permission notice shall be included in all
+
copies or substantial portions of the Software.
+
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+
SOFTWARE.
+9
Makefile
···
+
#!/usr/bin/make -f
+
+
PWD=$(shell pwd)
+
+
build:
+
go build -o servmon-bin .
+
+
install:
+
go install .
+23
README.md
···
+
# Servmond
+
+
KISS server monitoring tool with email alerts.
+
+
Monitors:
+
+
- [x] CPU
+
- [x] Memory
+
- [x] HTTP Health check
+
- [ ] Disk
+
- [ ] Docker
+
+
## Installation
+
+
```bash
+
go install github.com/julienrbrt/servmon@latest
+
```
+
+
## How to use
+
+
```bash
+
servmon --help
+
```
+21
config.example.yaml
···
+
alert_thresholds:
+
cpu:
+
threshold: 90
+
duration: 5m0s
+
cooldown: 30m0s
+
memory:
+
threshold: 80
+
cooldown: 30m0s
+
http:
+
url: http://localhost:8080/health
+
timeout: 5s
+
sample_rate: 10
+
failure_threshold: 20
+
check_interval: 1m0s
+
cooldown: 15m0s
+
email:
+
smtp_server: smtp.example.com
+
from: alerts@example.com
+
to: admin@example.com
+
username: alertuser
+
password: alertpassword
+109
config.go
···
+
package main
+
+
import (
+
"fmt"
+
"os"
+
"time"
+
+
"gopkg.in/yaml.v3"
+
)
+
+
// Config is a struct that holds the configuration for the monitoring service.
+
type Config struct {
+
AlertThresholds Thresholds `yaml:"alert_thresholds"`
+
Email Email `yaml:"email"`
+
}
+
+
type Thresholds struct {
+
CPU CPU `yaml:"cpu"`
+
Memory Memory `yaml:"memory"`
+
HTTP HTTP `yaml:"http"`
+
}
+
+
type CPU struct {
+
Threshold float64 `yaml:"threshold"`
+
Duration time.Duration `yaml:"duration"`
+
Cooldown time.Duration `yaml:"cooldown"`
+
}
+
+
type Memory struct {
+
Threshold float64 `yaml:"threshold"`
+
Cooldown time.Duration `yaml:"cooldown"`
+
}
+
+
type HTTP struct {
+
URL string `yaml:"url"`
+
Timeout time.Duration `yaml:"timeout"`
+
SampleRate int `yaml:"sample_rate"`
+
FailureThreshold float64 `yaml:"failure_threshold"`
+
CheckInterval time.Duration `yaml:"check_interval"`
+
Cooldown time.Duration `yaml:"cooldown"`
+
}
+
+
type Email struct {
+
SMTPServer string `yaml:"smtp_server"`
+
From string `yaml:"from"`
+
To string `yaml:"to"`
+
Username string `yaml:"username"`
+
Password string `yaml:"password"`
+
}
+
+
func (c *Config) Save(path string) error {
+
out, err := yaml.Marshal(c)
+
if err != nil {
+
return fmt.Errorf("failed to marshal config: %w", err)
+
}
+
+
if err := os.WriteFile(path, out, 0644); err != nil {
+
return fmt.Errorf("error generating sample config: %w", err)
+
}
+
+
return nil
+
}
+
+
// defaultConfig returns a default configuration for the monitoring service.
+
func defaultConfig() *Config {
+
return &Config{
+
AlertThresholds: Thresholds{
+
CPU: CPU{
+
Threshold: 90,
+
Duration: 5 * time.Minute,
+
Cooldown: 30 * time.Minute,
+
},
+
Memory: Memory{
+
Threshold: 80,
+
Cooldown: 30 * time.Minute,
+
},
+
HTTP: HTTP{
+
URL: "http://localhost:8080/health",
+
Timeout: 5 * time.Second,
+
SampleRate: 10,
+
FailureThreshold: 20,
+
CheckInterval: 1 * time.Minute,
+
Cooldown: 15 * time.Minute,
+
},
+
},
+
Email: Email{
+
SMTPServer: "smtp.example.com",
+
From: "alerts@example.com",
+
To: "admin@example.com",
+
Username: "alertuser",
+
Password: "alertpassword",
+
},
+
}
+
}
+
+
// loadConfig loads a configuration from a file.
+
func loadConfig(path string) (*Config, error) {
+
data, err := os.ReadFile(path)
+
if err != nil {
+
return nil, fmt.Errorf("error reading config file: %w", err)
+
}
+
+
var cfg Config
+
if err := yaml.Unmarshal(data, &cfg); err != nil {
+
return nil, fmt.Errorf("error unmarshaling config: %w", err)
+
}
+
+
return &cfg, nil
+
}
+42
email.go
···
+
package main
+
+
import (
+
"fmt"
+
"log"
+
+
"github.com/wneessen/go-mail"
+
)
+
+
// sendEmail sends an alert email using the configuration
+
func sendEmail(subject, body string, cfg *Config) error {
+
msg := mail.NewMsg()
+
if err := msg.From(cfg.Email.From); err != nil {
+
return fmt.Errorf("failed to set FROM address: %w", err)
+
}
+
if err := msg.To(cfg.Email.To); err != nil {
+
return fmt.Errorf("failed to set TO address: %w", err)
+
}
+
+
msg.Subject(fmt.Sprintf("[ServMon Alert] %s", subject))
+
msg.SetBodyString(mail.TypeTextPlain, body)
+
+
// Create SMTP client with configuration
+
client, err := mail.NewClient(
+
cfg.Email.SMTPServer,
+
mail.WithSMTPAuth(mail.SMTPAuthPlain),
+
mail.WithTLSPortPolicy(mail.TLSMandatory),
+
mail.WithUsername(cfg.Email.Username),
+
mail.WithPassword(cfg.Email.Password),
+
)
+
if err != nil {
+
return fmt.Errorf("failed to create SMTP client: %w", err)
+
}
+
+
// Send the email
+
if err := client.DialAndSend(msg); err != nil {
+
return fmt.Errorf("failed to send email: %w", err)
+
}
+
+
log.Printf("Email alert sent successfully: %s", subject)
+
return nil
+
}
+25
go.mod
···
+
module github.com/julienrbrt/servmon
+
+
go 1.23.4
+
+
require (
+
github.com/shirou/gopsutil/v4 v4.24.12
+
github.com/spf13/cobra v1.8.1
+
github.com/wneessen/go-mail v0.6.1
+
gopkg.in/yaml.v3 v3.0.1
+
)
+
+
require (
+
github.com/ebitengine/purego v0.8.1 // indirect
+
github.com/go-ole/go-ole v1.2.6 // indirect
+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+
github.com/spf13/pflag v1.0.5 // indirect
+
github.com/tklauser/go-sysconf v0.3.14 // indirect
+
github.com/tklauser/numcpus v0.8.0 // indirect
+
github.com/yusufpapurcu/wmi v1.2.4 // indirect
+
golang.org/x/crypto v0.32.0 // indirect
+
golang.org/x/sys v0.29.0 // indirect
+
golang.org/x/text v0.21.0 // indirect
+
)
+108
go.sum
···
+
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
+
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
+
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
+
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
+
github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
+
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
+
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
+
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
+
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
+
github.com/wneessen/go-mail v0.6.1 h1:cDGqlGuEEhdILRe53VFzmM9WBk8Xh/QMvbO0oxrNJB4=
+
github.com/wneessen/go-mail v0.6.1/go.mod h1:G702XlFhzHV0Z4w9j2VsH5K9dJDvj0hx+yOOp1oX9vc=
+
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
+
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
+
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
+
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
+
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
+
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
+
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/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/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/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
+
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
+
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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
+
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
+
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
+
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
+
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
+
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+
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/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
+
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
+
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+66
main.go
···
+
package main
+
+
import (
+
"fmt"
+
"os"
+
"path"
+
+
"github.com/spf13/cobra"
+
)
+
+
var (
+
version = "1.0.0"
+
flagConfig = "config"
+
cfgFile string
+
)
+
+
func main() {
+
homeDir, err := os.UserHomeDir()
+
if err != nil {
+
fmt.Fprintf(os.Stderr, "error getting user home directory: %v", err)
+
os.Exit(1)
+
}
+
+
rootCmd := &cobra.Command{
+
Use: "servmon",
+
Short: "Server Monitoring Tool with TUI and Alerts",
+
Version: version,
+
RunE: func(cmd *cobra.Command, args []string) error {
+
cfgPath, err := cmd.Flags().GetString(flagConfig)
+
if err != nil {
+
return fmt.Errorf("error getting flag %s: %v", flagConfig, err)
+
}
+
+
if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
+
cfg := defaultConfig()
+
if err := cfg.Save(cfgFile); err != nil {
+
return err
+
}
+
+
cmd.Println("Configuration file generated at", cfgFile)
+
return nil
+
} else if err != nil {
+
return fmt.Errorf("error checking config file: %v", err)
+
}
+
+
cfg, err := loadConfig(cfgPath)
+
if err != nil {
+
return err
+
}
+
+
go monitorCPU(cfg)
+
go monitorMemory(cfg)
+
go monitorHTTP(cfg)
+
+
select {} // keep alive
+
},
+
}
+
+
rootCmd.CompletionOptions.DisableDefaultCmd = true
+
rootCmd.PersistentFlags().StringVar(&cfgFile, flagConfig, path.Join(homeDir, ".servmon.yaml"), "config file")
+
+
if err := rootCmd.Execute(); err != nil {
+
fmt.Fprint(os.Stderr, err)
+
os.Exit(1)
+
}
+
}
+144
monitor.go
···
+
package main
+
+
import (
+
"context"
+
"fmt"
+
"log"
+
"net/http"
+
"time"
+
+
"github.com/shirou/gopsutil/v4/cpu"
+
"github.com/shirou/gopsutil/v4/mem"
+
)
+
+
func monitorCPU(cfg *Config) {
+
log.Printf("Monitoring CPU usage with threshold %.2f%% and cooldown %v", cfg.AlertThresholds.CPU.Threshold, cfg.AlertThresholds.CPU.Cooldown)
+
+
alertCooldown := time.NewTimer(cfg.AlertThresholds.CPU.Cooldown)
+
for {
+
percent, err := cpu.Percent(time.Duration(1)*time.Second, false)
+
if err != nil {
+
log.Printf("Error getting CPU usage: %v", err)
+
time.Sleep(1 * time.Second)
+
continue
+
}
+
+
// Average CPU usage across all cores
+
var total float64
+
for _, p := range percent {
+
total += p
+
}
+
+
avg := total / float64(len(percent))
+
+
if avg > cfg.AlertThresholds.CPU.Threshold {
+
// Check if we're within the cooldown period
+
select {
+
case <-alertCooldown.C:
+
// Cooldown expired, check again
+
alertCooldown.Reset(cfg.AlertThresholds.CPU.Cooldown)
+
default:
+
// Within cooldown, skip alert
+
time.Sleep(1 * time.Second)
+
continue
+
}
+
+
err := sendEmail(fmt.Sprintf("CPU Usage Alert: %.2f%%", avg),
+
fmt.Sprintf("CPU usage of %.2f%% has exceeded the threshold of %.2f%%", avg, cfg.AlertThresholds.CPU.Threshold), cfg)
+
if err != nil {
+
log.Printf("Error sending email: %v", err)
+
}
+
}
+
+
time.Sleep(time.Duration(1) * time.Second)
+
}
+
}
+
+
func monitorMemory(cfg *Config) {
+
log.Printf("Monitoring memory usage with threshold %.2f%% and cooldown %v", cfg.AlertThresholds.Memory.Threshold, cfg.AlertThresholds.Memory.Cooldown)
+
+
alertCooldown := time.NewTimer(cfg.AlertThresholds.Memory.Cooldown)
+
for {
+
vm, err := mem.VirtualMemory()
+
if err != nil {
+
log.Printf("Error getting memory usage: %v", err)
+
time.Sleep(1 * time.Second)
+
continue
+
}
+
+
usedPercent := vm.UsedPercent
+
+
if usedPercent > cfg.AlertThresholds.Memory.Threshold {
+
// Check if we're within the cooldown period
+
select {
+
case <-alertCooldown.C:
+
// Cooldown expired, check again
+
alertCooldown.Reset(cfg.AlertThresholds.Memory.Cooldown)
+
default:
+
// Within cooldown, skip alert
+
time.Sleep(1 * time.Second)
+
continue
+
}
+
+
err := sendEmail(fmt.Sprintf("Memory Usage Alert: %.2f%%", usedPercent),
+
fmt.Sprintf("Memory usage of %.2f%% has exceeded the threshold of %.2f%%", usedPercent, cfg.AlertThresholds.Memory.Threshold), cfg)
+
if err != nil {
+
log.Printf("Error sending email: %v", err)
+
}
+
}
+
+
time.Sleep(time.Duration(1) * time.Second)
+
}
+
}
+
+
func monitorHTTP(cfg *Config) {
+
log.Printf("Monitoring HTTP checks (%s) with threshold %.2f%% and cooldown %v", cfg.AlertThresholds.HTTP.URL, cfg.AlertThresholds.HTTP.FailureThreshold, cfg.AlertThresholds.HTTP.Cooldown)
+
+
alertCooldown := time.NewTimer(cfg.AlertThresholds.HTTP.Cooldown)
+
client := &http.Client{
+
Timeout: cfg.AlertThresholds.HTTP.Timeout,
+
}
+
+
for {
+
// Wait for check interval
+
time.Sleep(cfg.AlertThresholds.HTTP.CheckInterval)
+
+
// Perform HTTP checks
+
failureCount := 0
+
for i := 0; i < cfg.AlertThresholds.HTTP.SampleRate; i++ {
+
req, err := http.NewRequest("GET", cfg.AlertThresholds.HTTP.URL, nil)
+
if err != nil {
+
failureCount++
+
continue
+
}
+
+
ctx, cancel := context.WithTimeout(context.Background(), cfg.AlertThresholds.HTTP.Timeout)
+
defer cancel()
+
+
resp, err := client.Do(req.WithContext(ctx))
+
if err != nil || resp.StatusCode >= 400 {
+
failureCount++
+
}
+
}
+
+
// Calculate failure rate
+
failureRate := (float64(failureCount) / float64(cfg.AlertThresholds.HTTP.SampleRate)) * 100
+
if failureRate > cfg.AlertThresholds.HTTP.FailureThreshold {
+
// Check if we're within the cooldown period
+
select {
+
case <-alertCooldown.C:
+
// Cooldown expired, check again
+
alertCooldown.Reset(cfg.AlertThresholds.HTTP.Cooldown)
+
default:
+
// Within cooldown, skip alert
+
continue
+
}
+
+
err := sendEmail(fmt.Sprintf("HTTP Failure Alert: %.2f%%", failureRate),
+
fmt.Sprintf("HTTP failure rate of %.2f%% has exceeded the threshold of %.2f%%", failureRate, cfg.AlertThresholds.HTTP.FailureThreshold), cfg)
+
if err != nil {
+
log.Printf("Error sending email: %v", err)
+
}
+
}
+
}
+
}