├── logs └── .gitkeep ├── track ├── delayer.go ├── exp_backoff.go └── tracker.go ├── docker-compose.yml ├── dial ├── netaddress.go └── dialer.go ├── notify ├── sms.go ├── notifier.go ├── telegram.go ├── pushover.go ├── webook.go ├── slack.go └── email.go ├── validate └── validator.go ├── .gitignore ├── server.go ├── config.go ├── Dockerfile ├── cmd └── gossm │ └── main.go ├── LICENSE ├── statusdata.go ├── logger └── logger.go ├── settings.go ├── configs └── default.json ├── validate.go ├── http.go ├── monitor.go └── README.md /logs/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /track/delayer.go: -------------------------------------------------------------------------------- 1 | package track 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // Delayer is used to delay some action 8 | type Delayer interface { 9 | Delay() time.Duration 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | 3 | services: 4 | gossm: 5 | build: ./ 6 | ports: 7 | - "8080:8080" 8 | volumes: 9 | - ./configs:/configs 10 | - ./logs:/var/log/gossum -------------------------------------------------------------------------------- /dial/netaddress.go: -------------------------------------------------------------------------------- 1 | package dial 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // NetAddress is transport unit for Dialer 8 | type NetAddress struct { 9 | Network string 10 | Address string 11 | } 12 | 13 | // NetAddressTimeout is tuple of NetAddress and attached Timeout 14 | type NetAddressTimeout struct { 15 | NetAddress 16 | Timeout time.Duration 17 | } 18 | -------------------------------------------------------------------------------- /notify/sms.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | type SmsSettings struct { 4 | Sms string `json:"sms"` 5 | } 6 | 7 | type SmsNotifier struct { 8 | Settings *SmsSettings 9 | } 10 | 11 | func (s *SmsNotifier) Notify(text string) error { 12 | // TODO 13 | return nil 14 | } 15 | 16 | func (ss *SmsSettings) Validate() error { 17 | // TODO 18 | return nil 19 | } 20 | 21 | func (s *SmsNotifier) String() string { 22 | // TODO 23 | return "" 24 | } 25 | -------------------------------------------------------------------------------- /validate/validator.go: -------------------------------------------------------------------------------- 1 | package validate 2 | 3 | // Validator is used to validate something 4 | type Validator interface { 5 | // Validate returns nil if valid, error if invalid 6 | Validate() error 7 | } 8 | 9 | // ValidateAll validates all validators 10 | func ValidateAll(validators ...Validator) error { 11 | for _, validator := range validators { 12 | if err := validator.Validate(); err != nil { 13 | return err 14 | } 15 | } 16 | return nil 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.dll 4 | *.so 5 | *.dylib 6 | 7 | # Test binary, build with `go test -c` 8 | *.test 9 | 10 | # Output of the go coverage tool, specifically when used with LiteIDE 11 | *.out 12 | 13 | # Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 14 | .glide/ 15 | 16 | # GOSSM 17 | logs/* 18 | 19 | cmd/gossm/gossm 20 | cmd/gossm/log.txt 21 | cmd/gossm/myconfig.json 22 | 23 | gossm 24 | log.txt 25 | myconfig.json 26 | -------------------------------------------------------------------------------- /notify/notifier.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | // Initializer is used to initialize something 4 | type Initializer interface { 5 | Initialize() 6 | } 7 | 8 | // Notifier is used to send messages 9 | type Notifier interface { 10 | // Notify sends text over notifier, returns error message if failed 11 | Notify(text string) error 12 | } 13 | 14 | type Notifiers []Notifier 15 | 16 | func (notifiers Notifiers) NotifyAll(text string) { 17 | for _, notifier := range notifiers { 18 | go notifier.Notify(text) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /server.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type Servers []*Server 8 | 9 | type Server struct { 10 | Name string `json:"name"` 11 | IPAddress string `json:"ipAddress"` 12 | Port int `json:"port"` 13 | Protocol string `json:"protocol"` 14 | CheckInterval int `json:"checkInterval"` 15 | Timeout int `json:"timeout"` 16 | } 17 | 18 | func (s *Server) String() string { 19 | return fmt.Sprintf("%s %s:%d", s.Protocol, s.IPAddress, s.Port) 20 | } 21 | 22 | func (s *Server) MarshalText() (text []byte, err error) { 23 | return []byte(s.String()), nil 24 | } 25 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/ssimunic/gossm/validate" 7 | ) 8 | 9 | type Config struct { 10 | Servers Servers `json:"servers"` 11 | Settings *Settings `json:"settings"` 12 | } 13 | 14 | // NewConfig returns pointer to Config which is created from provided JSON data. 15 | // Guarantees to be validated. 16 | func NewConfig(jsonData []byte) *Config { 17 | config := &Config{} 18 | err := json.Unmarshal(jsonData, config) 19 | if err != nil { 20 | panic("error parsing json configuration data") 21 | } 22 | if err := validate.ValidateAll(config); err != nil { 23 | panic(err) 24 | } 25 | return config 26 | } 27 | -------------------------------------------------------------------------------- /track/exp_backoff.go: -------------------------------------------------------------------------------- 1 | package track 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // ExpBackoff is used as delayer, implements exponential backoff algorithm 8 | type ExpBackoff struct { 9 | counter int 10 | base int 11 | } 12 | 13 | func calculateExponential(base, counter int) int { 14 | if counter == 0 { 15 | return 1 16 | } 17 | return base * calculateExponential(base, counter-1) 18 | } 19 | 20 | // Delay returns seconds 21 | func (e *ExpBackoff) Delay() time.Duration { 22 | e.counter++ 23 | return time.Duration(calculateExponential(e.base, e.counter)) * time.Second 24 | } 25 | 26 | // NewExpBackoff returns pointer to new ExpBackoff 27 | // base is meant to be seconds 28 | func NewExpBackoff(base int) *ExpBackoff { 29 | return &ExpBackoff{ 30 | base: base, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:edge 2 | 3 | ENV GOPATH /go 4 | ENV PATH /go/src/github.com/ssimunic/gossm/bin:$PATH 5 | 6 | ADD . /go/src/github.com/ssimunic/gossm 7 | 8 | RUN echo "http://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories \ 9 | && apk add --no-cache --update bash ca-certificates \ 10 | && apk add --no-cache --virtual .build-deps go gcc git libc-dev \ 11 | && mkdir -p /configs /usr/local/bin /var/log/gossm \ 12 | && go get github.com/gregdel/pushover \ 13 | && cd /go/src/github.com/ssimunic/gossm \ 14 | && go build -v -o /usr/local/bin/gossm cmd/gossm/main.go \ 15 | && apk del --purge .build-deps \ 16 | && rm -rf /var/cache/apk* 17 | 18 | ADD configs /configs 19 | 20 | CMD ["gossm", "-config", "/configs/default.json", "-http", ":8080", "-log", "/var/log/gossm/gossm.log"] 21 | 22 | EXPOSE 8080 23 | -------------------------------------------------------------------------------- /cmd/gossm/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "io/ioutil" 6 | "time" 7 | 8 | "github.com/ssimunic/gossm" 9 | "github.com/ssimunic/gossm/logger" 10 | ) 11 | 12 | var configPath = flag.String("config", "configs/default.json", "configuration file") 13 | var logPath = flag.String("log", "logs/from-"+time.Now().Format("2006-01-02")+".log", "log file") 14 | var address = flag.String("http", ":8080", "address for http server") 15 | var nolog = flag.Bool("nolog", false, "disable logging to file only") 16 | var logfilter = flag.String("logfilter", "", "text to filter log by (both console and file)") 17 | 18 | func main() { 19 | flag.Parse() 20 | jsonData, err := ioutil.ReadFile(*configPath) 21 | if err != nil { 22 | panic("error reading from configuration file") 23 | } 24 | 25 | if *nolog == true { 26 | logger.Disable() 27 | } 28 | 29 | if *logfilter != "" { 30 | logger.Filter(*logfilter) 31 | } 32 | 33 | logger.SetFilename(*logPath) 34 | 35 | config := gossm.NewConfig(jsonData) 36 | monitor := gossm.NewMonitor(config) 37 | go gossm.RunHttp(*address, monitor) 38 | monitor.Run() 39 | } 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Silvio Simunic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /statusdata.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type ServerStatusData struct { 9 | rwmu sync.RWMutex 10 | ServerStatus map[*Server][]*statusAtTime `json:"serverStatus"` 11 | } 12 | 13 | type statusAtTime struct { 14 | Time time.Time `json:"time"` 15 | // bool represent server online or offline 16 | Status bool `json:"online"` 17 | } 18 | 19 | func NewServerStatusData(servers Servers) *ServerStatusData { 20 | serverStatusData := &ServerStatusData{ 21 | ServerStatus: make(map[*Server][]*statusAtTime), 22 | } 23 | 24 | for _, server := range servers { 25 | serverStatusData.ServerStatus[server] = make([]*statusAtTime, 0, 100) 26 | } 27 | 28 | return serverStatusData 29 | } 30 | 31 | // SetStatusAtTimeForServer updates map with new entry containing current time and server status at that time 32 | func (s *ServerStatusData) SetStatusAtTimeForServer(server *Server, timeNow time.Time, status bool) { 33 | s.rwmu.Lock() 34 | defer s.rwmu.Unlock() 35 | s.ServerStatus[server] = append(s.ServerStatus[server], &statusAtTime{Time: timeNow, Status: status}) 36 | } 37 | 38 | func (s *ServerStatusData) GetServerStatus() map[*Server][]*statusAtTime { 39 | s.rwmu.RLock() 40 | defer s.rwmu.RUnlock() 41 | return s.ServerStatus 42 | } 43 | -------------------------------------------------------------------------------- /notify/telegram.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | type TelegramSettings struct { 10 | BotToken string `json:"botToken"` 11 | ChatID string `json:"chatId"` 12 | } 13 | 14 | type TelegramNotifier struct { 15 | Settings *TelegramSettings 16 | } 17 | 18 | func (s *TelegramNotifier) Notify(text string) error { 19 | values := url.Values{} 20 | values.Add("chat_id", s.Settings.ChatID) 21 | values.Add("parse_mode", "markdown") 22 | values.Add("text", "*[Error]* _GOSSM_\nServer "+text+" not reached.") 23 | _, err := http.PostForm(fmt.Sprintf("https://api.telegram.org/bot%s/sendMessage", s.Settings.BotToken), values) 24 | return err 25 | } 26 | 27 | func (e *TelegramNotifier) Initialize() { 28 | } 29 | 30 | func (ts *TelegramSettings) Validate() error { 31 | errTelegramProperty := func(property string) error { 32 | return fmt.Errorf("missing telegram property %s", property) 33 | } 34 | switch { 35 | case ts.BotToken == "": 36 | return errTelegramProperty("bot_token") 37 | case ts.ChatID == "": 38 | return errTelegramProperty("chat_id") 39 | } 40 | return nil 41 | } 42 | 43 | func (t *TelegramNotifier) String() string { 44 | return fmt.Sprintf("Telegram Bot %s with ChatID %s", t.Settings.BotToken, t.Settings.ChatID) 45 | } 46 | -------------------------------------------------------------------------------- /track/tracker.go: -------------------------------------------------------------------------------- 1 | package track 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | // TimeTracker is used to keep track of something 8 | type TimeTracker struct { 9 | delayer Delayer 10 | nextTime time.Time 11 | // counter is used to remember how many times has delayer been ran 12 | counter int 13 | } 14 | 15 | // IsReady checks if current time is after nextTime 16 | // On first check before calling SetNext, always true due to nextTime having zero value 17 | func (t *TimeTracker) IsReady() bool { 18 | if time.Now().After(t.nextTime) { 19 | return true 20 | } 21 | return false 22 | } 23 | 24 | // SetNext updates nextTime based on delayer implementation 25 | // Returns delay and time at which will current time be after it 26 | func (t *TimeTracker) SetNext() (time.Duration, time.Time) { 27 | t.counter++ 28 | nextDelay := t.delayer.Delay() 29 | t.nextTime = time.Now().Add(nextDelay) 30 | return nextDelay, t.nextTime 31 | } 32 | 33 | // NewTracker returns pointer to new TimeTracker and sets its Delayer 34 | func NewTracker(delayer Delayer) *TimeTracker { 35 | return &TimeTracker{delayer: delayer} 36 | } 37 | 38 | // HasBeenRan checks how many times has time delayer and returns true if ever ran 39 | func (t *TimeTracker) HasBeenRan() bool { 40 | if t.counter > 0 { 41 | return true 42 | } 43 | return false 44 | } 45 | -------------------------------------------------------------------------------- /dial/dialer.go: -------------------------------------------------------------------------------- 1 | package dial 2 | 3 | import ( 4 | "net" 5 | ) 6 | 7 | // Dialer is used to test connections 8 | type Dialer struct { 9 | semaphore chan struct{} 10 | } 11 | 12 | // Status saves information about connection 13 | type Status struct { 14 | Ok bool 15 | Err error 16 | } 17 | 18 | // NewDialer returns pointer to new Dialer 19 | func NewDialer(concurrentConnections int) *Dialer { 20 | return &Dialer{ 21 | semaphore: make(chan struct{}, concurrentConnections), 22 | } 23 | } 24 | 25 | // NewWorker is used to send address over NetAddressTimeout to make request and receive status over DialerStatus 26 | // Blocks until slot in semaphore channel for concurrency is free 27 | func (d *Dialer) NewWorker() (chan<- NetAddressTimeout, <-chan Status) { 28 | netAddressTimeoutCh := make(chan NetAddressTimeout) 29 | dialerStatusCh := make(chan Status) 30 | 31 | d.semaphore <- struct{}{} 32 | go func() { 33 | netAddressTimeout := <-netAddressTimeoutCh 34 | conn, err := net.DialTimeout(netAddressTimeout.Network, netAddressTimeout.Address, netAddressTimeout.Timeout) 35 | 36 | dialerStatus := Status{} 37 | 38 | if err != nil { 39 | dialerStatus.Ok = false 40 | dialerStatus.Err = err 41 | } else { 42 | dialerStatus.Ok = true 43 | conn.Close() 44 | } 45 | dialerStatusCh <- dialerStatus 46 | <-d.semaphore 47 | }() 48 | 49 | return netAddressTimeoutCh, dialerStatusCh 50 | } 51 | -------------------------------------------------------------------------------- /notify/pushover.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/gregdel/pushover" 7 | ) 8 | 9 | type PushoverSettings struct { 10 | UserKey string `json:"userKey"` 11 | AppToken string `json:"appToken"` 12 | } 13 | 14 | type PushoverNotifier struct { 15 | Settings *PushoverSettings 16 | } 17 | 18 | func (s *PushoverNotifier) Notify(text string) error { 19 | // Create a new pushover app with a token 20 | app := pushover.New(s.Settings.AppToken) 21 | // Create a new recipient 22 | recipient := pushover.NewRecipient(s.Settings.UserKey) 23 | // Create the message to send 24 | message := pushover.NewMessageWithTitle(text+" not reached", "GOSSM Notification") 25 | 26 | // Send the message to the recipient 27 | _, err := app.SendMessage(message, recipient) 28 | return err 29 | } 30 | 31 | func (e *PushoverNotifier) Initialize() { 32 | } 33 | 34 | func (ts *PushoverSettings) Validate() error { 35 | errPushoverProperty := func(property string) error { 36 | return fmt.Errorf("missing Pushover property %s", property) 37 | } 38 | switch { 39 | case ts.UserKey == "": 40 | return errPushoverProperty("user_key") 41 | case ts.AppToken == "": 42 | return errPushoverProperty("app_token") 43 | } 44 | return nil 45 | } 46 | 47 | func (t *PushoverNotifier) String() string { 48 | return fmt.Sprintf("Pushover: %s with appToken %s", t.Settings.UserKey, t.Settings.AppToken) 49 | } 50 | -------------------------------------------------------------------------------- /notify/webook.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "net/url" 7 | ) 8 | 9 | type WebhookSettings struct { 10 | Url string `json:"url"` 11 | Method string `json:"method"` 12 | } 13 | 14 | type WebhookNotifier struct { 15 | Settings *WebhookSettings 16 | } 17 | 18 | func (w *WebhookNotifier) Notify(srv string) error { 19 | switch w.Settings.Method { 20 | case "GET": 21 | u, err := url.Parse(w.Settings.Url) 22 | if err != nil { 23 | return err 24 | } 25 | u.Query().Add("server", srv) 26 | _, err = http.Get(u.String()) 27 | return err 28 | case "POST": 29 | values := url.Values{} 30 | values.Add("server", srv) 31 | _, err := http.PostForm(w.Settings.Url, values) 32 | return err 33 | default: 34 | return fmt.Errorf("Invalid Method %s", w.Settings.Method) 35 | } 36 | } 37 | 38 | func (w *WebhookNotifier) Initialize() { 39 | } 40 | 41 | func (ws *WebhookSettings) Validate() error { 42 | errWebhookProperty := func(property string) error { 43 | return fmt.Errorf("missing webhook property %s", property) 44 | } 45 | switch { 46 | case ws.Url == "": 47 | return errWebhookProperty("url") 48 | case ws.Method == "": 49 | return errWebhookProperty("method") 50 | } 51 | return nil 52 | } 53 | 54 | func (w *WebhookNotifier) String() string { 55 | return fmt.Sprintf("Webhook with method %s to URL %s", w.Settings.Method, w.Settings.Url) 56 | } 57 | -------------------------------------------------------------------------------- /notify/slack.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | ) 10 | 11 | type SlackSettings struct { 12 | BearerToken string `json:"bearerToken"` 13 | ChannelID string `json:"channelId"` 14 | } 15 | 16 | type SlackNotifier struct { 17 | Settings *SlackSettings 18 | } 19 | 20 | func (s *SlackNotifier) Notify(text string) error { 21 | payload := map[string]interface{}{"channel": s.Settings.ChannelID, "text": "GOSSM Notification: " + text + " not reached"} 22 | bytes, _ := json.Marshal(payload) 23 | 24 | client := &http.Client{} 25 | r, _ := http.NewRequest(http.MethodPost, "https://slack.com/api/chat.postMessage", strings.NewReader(string(bytes))) 26 | r.Header.Add("Authorization", "Bearer "+s.Settings.BearerToken) 27 | r.Header.Add("Content-Type", "application/json") 28 | r.Header.Add("Content-Length", strconv.Itoa(len(string(bytes)))) 29 | 30 | _, err := client.Do(r) 31 | return err 32 | } 33 | 34 | func (s *SlackNotifier) Initialize() { 35 | } 36 | 37 | func (ss *SlackSettings) Validate() error { 38 | errSlackProperty := func(property string) error { 39 | return fmt.Errorf("missing slack property %s", property) 40 | } 41 | switch { 42 | case ss.BearerToken == "": 43 | return errSlackProperty("bearerToken") 44 | case ss.ChannelID == "": 45 | return errSlackProperty("channelId") 46 | } 47 | return nil 48 | } 49 | 50 | func (s *SlackNotifier) String() string { 51 | return fmt.Sprintf("Slack Bot %s on ChannelID %s", s.Settings.BearerToken, s.Settings.ChannelID) 52 | } 53 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | "strings" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | var ( 13 | enabledFileLog = true 14 | logFilename = "log.txt" 15 | mu sync.Mutex 16 | filter string 17 | ) 18 | 19 | // Log writes text to standard output and file 20 | func Log(text string) { 21 | if filter == "" || (filter != "" && strings.Contains(text, filter)) { 22 | log.Print(text) 23 | 24 | if !enabledFileLog { 25 | return 26 | } 27 | mu.Lock() 28 | defer mu.Unlock() 29 | if err := writeToFile(logFilename, text); err != nil { 30 | log.Println(err) 31 | } 32 | } 33 | } 34 | 35 | // Logln writes text with new line to standard output and file 36 | func Logln(v ...interface{}) { 37 | Log(fmt.Sprintln(v...)) 38 | } 39 | 40 | // Logf writes formated text to standard output and file 41 | func Logf(format string, v ...interface{}) { 42 | Log(fmt.Sprintf(format, v...)) 43 | } 44 | 45 | // SetFilename updates filename in which logs will be saved 46 | func SetFilename(fileName string) { 47 | logFilename = fileName 48 | } 49 | 50 | // Disable logging to file 51 | func Disable() { 52 | enabledFileLog = false 53 | } 54 | 55 | // Enable logging to file 56 | func Enable() { 57 | enabledFileLog = true 58 | } 59 | 60 | // Filter filters logs only that contain specific keyword 61 | func Filter(f string) { 62 | filter = f 63 | } 64 | 65 | // writeToFile writes text to fileName, creates new one if it doesn't exist 66 | func writeToFile(fileName, text string) error { 67 | file, err := os.OpenFile(logFilename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0600) 68 | if err != nil { 69 | return err 70 | } 71 | defer file.Close() 72 | text = fmt.Sprintf("%s %s", time.Now().String(), text) 73 | if _, err = file.WriteString(text); err != nil { 74 | return err 75 | } 76 | return nil 77 | } 78 | -------------------------------------------------------------------------------- /notify/email.go: -------------------------------------------------------------------------------- 1 | package notify 2 | 3 | import ( 4 | "fmt" 5 | "net/smtp" 6 | "strings" 7 | ) 8 | 9 | type EmailSettings struct { 10 | SMTP string 11 | Port int 12 | Username string 13 | Password string 14 | From string 15 | To []string 16 | } 17 | 18 | type EmailNotifier struct { 19 | Settings *EmailSettings 20 | auth smtp.Auth 21 | } 22 | 23 | func (es *EmailSettings) Validate() error { 24 | errEmailProperty := func(property string) error { 25 | return fmt.Errorf("missing email property %s", property) 26 | } 27 | switch { 28 | case es.Username == "": 29 | return errEmailProperty("username") 30 | case es.Password == "": 31 | return errEmailProperty("password") 32 | case es.SMTP == "": 33 | return errEmailProperty("smtp") 34 | case es.Port == 0: 35 | return errEmailProperty("port") 36 | case es.From == "": 37 | return errEmailProperty("from") 38 | case len(es.To) == 0: 39 | return errEmailProperty("to") 40 | } 41 | return nil 42 | } 43 | 44 | func (e *EmailNotifier) Initialize() { 45 | e.auth = smtp.PlainAuth( 46 | "", 47 | e.Settings.Username, 48 | e.Settings.Password, 49 | e.Settings.SMTP, 50 | ) 51 | } 52 | 53 | func (e *EmailNotifier) String() string { 54 | return fmt.Sprintf("email %s at %s:%d", e.Settings.Username, e.Settings.SMTP, e.Settings.Port) 55 | } 56 | 57 | func (e *EmailNotifier) Notify(text string) error { 58 | formattedReceipets := strings.Join(e.Settings.To, ", ") 59 | msg := "From: " + e.Settings.From + "\n" + 60 | "To: " + formattedReceipets + "\n" + 61 | "Subject: GOSSM Notification\n\n" + 62 | text + " not reached." 63 | 64 | err := smtp.SendMail( 65 | fmt.Sprintf("%s:%d", e.Settings.SMTP, e.Settings.Port), 66 | e.auth, 67 | e.Settings.From, 68 | e.Settings.To, 69 | []byte(msg), 70 | ) 71 | if err != nil { 72 | return fmt.Errorf("error sending email: %s", err) 73 | } 74 | return nil 75 | } 76 | -------------------------------------------------------------------------------- /settings.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "github.com/ssimunic/gossm/notify" 5 | ) 6 | 7 | type Settings struct { 8 | Monitor *MonitorSettings 9 | Notifications *NotificationSettings 10 | } 11 | 12 | type MonitorSettings struct { 13 | CheckInterval int `json:"checkInterval"` 14 | Timeout int `json:"timeout"` 15 | MaxConnections int `json:"maxConnections"` 16 | ExponentialBackoffSeconds int `json:"exponentialBackoffSeconds"` 17 | } 18 | 19 | type NotificationSettings struct { 20 | Email []*notify.EmailSettings `json:"email"` 21 | Sms []*notify.SmsSettings `json:"sms"` 22 | Telegram []*notify.TelegramSettings `json:"telegram"` 23 | Pushover []*notify.PushoverSettings `json:"pushover"` 24 | Slack []*notify.SlackSettings `json:"slack"` 25 | Webhook []*notify.WebhookSettings `json:"webhook"` 26 | } 27 | 28 | func (n *NotificationSettings) GetNotifiers() (notifiers notify.Notifiers) { 29 | for _, email := range n.Email { 30 | emailNotifier := ¬ify.EmailNotifier{Settings: email} 31 | notifiers = append(notifiers, emailNotifier) 32 | } 33 | for _, sms := range n.Sms { 34 | smsNotifier := ¬ify.SmsNotifier{Settings: sms} 35 | notifiers = append(notifiers, smsNotifier) 36 | } 37 | for _, telegram := range n.Telegram { 38 | telegramNotifier := ¬ify.TelegramNotifier{Settings: telegram} 39 | notifiers = append(notifiers, telegramNotifier) 40 | } 41 | for _, pushover := range n.Pushover { 42 | pushoverNotifier := ¬ify.PushoverNotifier{Settings: pushover} 43 | notifiers = append(notifiers, pushoverNotifier) 44 | } 45 | for _, slack := range n.Slack { 46 | slackNotifier := ¬ify.SlackNotifier{Settings: slack} 47 | notifiers = append(notifiers, slackNotifier) 48 | } 49 | for _, webhook := range n.Webhook { 50 | webhookNotifier := ¬ify.WebhookNotifier{Settings: webhook} 51 | notifiers = append(notifiers, webhookNotifier) 52 | } 53 | return 54 | } 55 | -------------------------------------------------------------------------------- /configs/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "notifications": { 4 | "email": [ 5 | { 6 | "smtp": "smtp.gmail.com", 7 | "port": 587, 8 | "username": "silvio.simunic@gmail.com", 9 | "password": "...", 10 | "from": "silvio.simunic@gmail.com", 11 | "to": [ 12 | "silvio.simunic@gmail.com" 13 | ] 14 | } 15 | ], 16 | "telegram": [ 17 | { 18 | "botToken": "123456:ABC-DEF1234...", 19 | "chatId": "12341234" 20 | } 21 | ], 22 | "pushover": [ 23 | { 24 | "userKey": "user_key", 25 | "appToken": "app_token" 26 | } 27 | ], 28 | "slack": [ 29 | { 30 | "bearerToken": "bearerToken", 31 | "channelId": "channelId" 32 | } 33 | ], 34 | "webhook": [ 35 | { 36 | "url": "url", 37 | "method": "GET" 38 | } 39 | ], 40 | "sms": [ 41 | { 42 | "sms": "todo" 43 | } 44 | ] 45 | }, 46 | "monitor": { 47 | "checkInterval": 15, 48 | "timeout": 5, 49 | "maxConnections": 50, 50 | "exponentialBackoffSeconds": 5 51 | } 52 | }, 53 | "servers": [ 54 | { 55 | "name":"Local Webserver 1", 56 | "ipAddress":"192.168.20.168", 57 | "port": 80, 58 | "protocol": "tcp", 59 | "checkInterval": 5, 60 | "timeout": 5 61 | }, 62 | { 63 | "name":"Test server 1", 64 | "ipAddress":"162.243.10.151", 65 | "port": 80, 66 | "protocol": "tcp", 67 | "checkInterval": 5, 68 | "timeout": 5 69 | }, 70 | { 71 | "name":"Test server 2", 72 | "ipAddress":"162.243.10.151", 73 | "port": 8080, 74 | "protocol": "tcp", 75 | "checkInterval": 5, 76 | "timeout": 5 77 | } 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /validate.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | func (c *Config) Validate() error { 8 | if err := c.Settings.Validate(); err != nil { 9 | return fmt.Errorf("invalid settings: %v", err) 10 | } 11 | if err := c.Servers.Validate(); err != nil { 12 | return fmt.Errorf("invalid servers: %v", err) 13 | } 14 | return nil 15 | } 16 | 17 | func (s *Settings) Validate() error { 18 | if err := s.Monitor.Validate(); err != nil { 19 | return fmt.Errorf("invalid monitor settings: %v", err) 20 | } 21 | if err := s.Notifications.Validate(); err != nil { 22 | return fmt.Errorf("invalid notification settings: %v", err) 23 | } 24 | return nil 25 | } 26 | 27 | func (ms *MonitorSettings) Validate() error { 28 | // ExponentialBackoffSeconds can be 0, which means when calculated, 29 | // delay for notifications will always be 1 second 30 | if ms.CheckInterval <= 0 || ms.MaxConnections <= 0 || ms.Timeout <= 0 || ms.ExponentialBackoffSeconds < 0 { 31 | return fmt.Errorf("monitor settings missing") 32 | } 33 | return nil 34 | } 35 | 36 | func (ns *NotificationSettings) Validate() error { 37 | for _, email := range ns.Email { 38 | if err := email.Validate(); err != nil { 39 | return fmt.Errorf("invalid email settings: %v", err) 40 | } 41 | } 42 | for _, sms := range ns.Sms { 43 | if err := sms.Validate(); err != nil { 44 | return fmt.Errorf("invalid sms settings: %v", err) 45 | } 46 | } 47 | for _, telegram := range ns.Telegram { 48 | if err := telegram.Validate(); err != nil { 49 | return fmt.Errorf("invalid telegram settings: %v", err) 50 | } 51 | } 52 | for _, slack := range ns.Slack { 53 | if err := slack.Validate(); err != nil { 54 | return fmt.Errorf("invalid slack settings: %v", err) 55 | } 56 | } 57 | for _, pushover := range ns.Pushover { 58 | if err := pushover.Validate(); err != nil { 59 | return fmt.Errorf("invalid pushover settings: %v", err) 60 | } 61 | } 62 | for _, webhook := range ns.Webhook { 63 | if err := webhook.Validate(); err != nil { 64 | return fmt.Errorf("invalid webhook settings: %v", err) 65 | } 66 | } 67 | return nil 68 | } 69 | 70 | func (servers Servers) Validate() error { 71 | if len(servers) == 0 { 72 | return fmt.Errorf("no servers found in config") 73 | } 74 | 75 | for _, server := range servers { 76 | if err := server.Validate(); err != nil { 77 | return fmt.Errorf("invalid server settings: %s", err) 78 | } 79 | 80 | } 81 | return nil 82 | } 83 | 84 | func (s *Server) Validate() error { 85 | errServerProperty := func(property string) error { 86 | return fmt.Errorf("missing server property %s", property) 87 | } 88 | switch { 89 | case s.Name == "": 90 | return errServerProperty("name") 91 | case s.IPAddress == "": 92 | return errServerProperty("ipAddress") 93 | case s.Port == 0: 94 | return errServerProperty("port") 95 | case s.Protocol == "": 96 | return errServerProperty("protocol") 97 | } 98 | return nil 99 | } 100 | -------------------------------------------------------------------------------- /http.go: -------------------------------------------------------------------------------- 1 | package gossm 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "html/template" 7 | "net/http" 8 | "time" 9 | ) 10 | 11 | func calculateServerUptime(statusAtTime []*statusAtTime) string { 12 | if len(statusAtTime) == 0 { 13 | return "unknown" 14 | } 15 | 16 | var sum float64 17 | 18 | for _, val := range statusAtTime { 19 | var i float64 20 | if val.Status { 21 | i = 1 22 | } else { 23 | i = 0 24 | } 25 | sum += i 26 | } 27 | 28 | return fmt.Sprintf("%.2f", sum/float64(len(statusAtTime))*100) 29 | } 30 | 31 | func lastStatus(statusAtTime []*statusAtTime) string { 32 | if len(statusAtTime) == 0 { 33 | return "Not yet checked" 34 | } 35 | lastChecked := statusAtTime[len(statusAtTime)-1] 36 | difference := time.Since(lastChecked.Time) 37 | status := "OK" 38 | if !lastChecked.Status { 39 | status = "ERR" 40 | } 41 | return fmt.Sprintf("%s, %.0f seconds ago", status, difference.Seconds()) 42 | } 43 | 44 | func RunHttp(address string, monitor *Monitor) { 45 | funcMap := template.FuncMap{ 46 | "calculateServerUptime": calculateServerUptime, 47 | "lastStatus": lastStatus, 48 | } 49 | 50 | t := template.Must(template.New("main").Funcs(funcMap).Parse(` 51 | 52 |
53 | 54 | 55 | 56 |{{ $server }}
tested {{ len $statusAtTime }} times
{{ $statusAtTime | lastStatus }}