Skip to content

Commit 5302f52

Browse files
authored
Merge pull request #7 from 0ne-zero/0-notification
Implement scalable notification mechanism
2 parents 2b4b36d + f6a6017 commit 5302f52

File tree

7 files changed

+282
-157
lines changed

7 files changed

+282
-157
lines changed

Makefile

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
OUTPUT_DIR = build
2-
SOURCE = ./
2+
SOURCE = ./cmd/radar-notif/
33

44
all: linux windows mac
55

66
linux:
77
GOOS=linux GOARCH=amd64 go build -o $(OUTPUT_DIR)/radar-linux $(SOURCE)
8+
cp icon.png $(OUTPUT_DIR)
89

910
windows:
1011
GOOS=windows GOARCH=amd64 go build -o $(OUTPUT_DIR)/radar-windows.exe $(SOURCE)
12+
cp icon.png $(OUTPUT_DIR)
1113

1214
mac:
1315
GOOS=darwin GOARCH=amd64 go build -o $(OUTPUT_DIR)/radar-mac $(SOURCE)
16+
cp icon.png $(OUTPUT_DIR)
1417

1518
clean:
1619
rm -rf $(OUTPUT_DIR)

cmd/radar-notif/main.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
package main
2+
3+
import (
4+
"flag"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
"sync"
10+
"time"
11+
12+
internal_flag "github.com/ohmydevops/arvancloud-radar-notif/internal/flag"
13+
"github.com/ohmydevops/arvancloud-radar-notif/internal/notification"
14+
"github.com/ohmydevops/arvancloud-radar-notif/radar"
15+
)
16+
17+
const ProgramName = "📡 Arvan Cloud Radar Monitor"
18+
19+
// Max consecutive errors to consider outage
20+
const maxConsecutiveErrorsForOutage = 3
21+
const notificationIconPath = "./icon.png"
22+
23+
var (
24+
DatacenterErrorCounts = make(map[radar.Datacenter]int)
25+
erroredDatacenters = make(map[radar.Datacenter]bool)
26+
mu sync.Mutex // protects DatacenterErrorCounts & erroredDatacenters
27+
)
28+
29+
func main() {
30+
cfg, err := internal_flag.ParseFlags()
31+
if err != nil {
32+
fmt.Println("❌", err)
33+
flag.Usage()
34+
os.Exit(1)
35+
}
36+
37+
if cfg.ShowServices {
38+
printServices()
39+
os.Exit(0)
40+
}
41+
42+
// Create notification manager
43+
notifiers := []notification.Notifier{
44+
notification.NewConsoleNotifier(),
45+
}
46+
if cfg.DesktopNotification {
47+
notifiers = append(notifiers, notification.NewDesktopNotofier(ProgramName, notificationIconPath))
48+
}
49+
notifiersManager := notification.NewNotofiersManager(notifiers)
50+
51+
fmt.Println(ProgramName)
52+
53+
fmt.Printf("✅ Monitoring service: %s\n\n", capitalizeFirst(cfg.Service))
54+
55+
// performDelay(cfg.CheckDelay)
56+
57+
for {
58+
fmt.Printf("⏰ %s\n", time.Now().Format("15:04:05"))
59+
60+
var wg sync.WaitGroup
61+
62+
for _, datacenter := range radar.AllDatacenters {
63+
wg.Add(1)
64+
go func(dc radar.Datacenter) {
65+
defer wg.Done()
66+
checkDatacenter(dc, radar.Service(cfg.Service), notifiersManager)
67+
}(datacenter)
68+
}
69+
70+
wg.Wait()
71+
72+
performDelay(cfg.CheckDelay)
73+
}
74+
}
75+
76+
// checkDatacenter handles checking & notification for a single datacenter
77+
func checkDatacenter(datacenter radar.Datacenter, service radar.Service, notifiersManager *notification.NotifiersManager) {
78+
79+
// Retrieve connectivity statistics between the datacenter and the service
80+
stats, err := radar.CheckDatacenterServiceStatistics(datacenter, service)
81+
if err != nil {
82+
fmt.Printf("⚠️ Statistics: %v from [%s]\n", err, datacenter)
83+
return
84+
}
85+
86+
// Prevents race condition
87+
mu.Lock()
88+
defer mu.Unlock()
89+
90+
if stats.IsAccessibleNow() {
91+
if erroredDatacenters[datacenter] {
92+
title := "🟢 Internet Restored"
93+
msg := fmt.Sprintf("%s is reachable again from %s", capitalizeFirst(string(service)), datacenter)
94+
95+
// Notify through notification mechanisms
96+
if err := notifiersManager.Notify(title, msg); err != nil {
97+
log.Printf("❌ Notification: %v from [%s]", err, datacenter)
98+
}
99+
}
100+
erroredDatacenters[datacenter] = false
101+
DatacenterErrorCounts[datacenter] = 0
102+
} else {
103+
DatacenterErrorCounts[datacenter]++
104+
if DatacenterErrorCounts[datacenter] >= maxConsecutiveErrorsForOutage && !erroredDatacenters[datacenter] {
105+
title := "🔴 Internet Outage"
106+
msg := fmt.Sprintf("%s is unreachable from %s", capitalizeFirst(string(service)), datacenter)
107+
108+
// Notify through notification mechanisms
109+
if err := notifiersManager.Notify(title, msg); err != nil {
110+
fmt.Printf("❌ Notification: %v from [%s]", err, datacenter)
111+
}
112+
erroredDatacenters[datacenter] = true
113+
}
114+
}
115+
}
116+
117+
// printServices prints available services
118+
func printServices() {
119+
fmt.Println("Available services:")
120+
for _, s := range radar.AllServices {
121+
fmt.Printf(" - %s\n", s)
122+
}
123+
124+
}
125+
126+
// performDelay sleeps for the specified number of minutes
127+
func performDelay(minutes int) {
128+
time.Sleep(time.Duration(minutes) * time.Minute)
129+
// time.Sleep(5 * time.Second)
130+
}
131+
132+
// capitalizeFirst makes the first letter uppercase
133+
func capitalizeFirst(s string) string {
134+
if len(s) == 0 {
135+
return s
136+
}
137+
return strings.ToUpper(s[:1]) + s[1:]
138+
}

flag.go

Lines changed: 0 additions & 39 deletions
This file was deleted.

internal/flag/flag.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package flag
2+
3+
import (
4+
"errors"
5+
"flag"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/ohmydevops/arvancloud-radar-notif/radar"
10+
)
11+
12+
const DefaultCheckDelay = 1
13+
const DefaultDesktopNotif = true
14+
15+
type Config struct {
16+
Service string
17+
ShowServices bool
18+
CheckDelay int // In minutes
19+
DesktopNotification bool
20+
}
21+
22+
func NewConfig() Config {
23+
return Config{
24+
CheckDelay: DefaultCheckDelay,
25+
DesktopNotification: DefaultDesktopNotif,
26+
}
27+
}
28+
29+
func ParseFlags() (*Config, error) {
30+
var cfg Config = NewConfig()
31+
32+
flag.StringVar(&cfg.Service, "service", "", "Service name to monitor (e.g. google, github, etc.)")
33+
flag.BoolVar(&cfg.ShowServices, "services", false, "Show list of available services")
34+
flag.IntVar(&cfg.CheckDelay, "delay", DefaultCheckDelay, "Delay between checks in minutes")
35+
36+
// Negative flag disables notifications, so bind to a local variable and invert it
37+
disableDesktopNotif := flag.Bool("no-desktop-notif", false, "Disable desktop notifications")
38+
39+
flag.Parse()
40+
41+
// Apply negative flag to DesktopNotification
42+
cfg.DesktopNotification = !(*disableDesktopNotif)
43+
44+
if cfg.ShowServices {
45+
return &cfg, nil
46+
}
47+
48+
// normalize to lowercase
49+
cfg.Service = strings.ToLower(cfg.Service)
50+
51+
if cfg.Service == "" {
52+
return nil, errors.New("must specify a service")
53+
}
54+
// Validate service
55+
if _, ok := radar.ParseService(cfg.Service); !ok {
56+
return nil, fmt.Errorf("invalid service: %s", cfg.Service)
57+
}
58+
59+
if cfg.CheckDelay < 1 {
60+
return nil, fmt.Errorf("delay must be greater than 0")
61+
}
62+
return &cfg, nil
63+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package notification
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/gen2brain/beeep"
7+
)
8+
9+
// Notifier is an interface for different notification backends.
10+
// Only cares about getting the final title + message.
11+
type Notifier interface {
12+
Notify(title string, message string) error
13+
}
14+
15+
// NotifiersManager combines multiple notifiers into one.
16+
type NotifiersManager struct {
17+
Notifiers []Notifier
18+
}
19+
20+
func NewNotofiersManager(notifiers []Notifier) *NotifiersManager {
21+
return &NotifiersManager{
22+
Notifiers: notifiers,
23+
}
24+
}
25+
26+
func (m *NotifiersManager) Notify(title, message string) error {
27+
for _, n := range m.Notifiers {
28+
if err := n.Notify(title, message); err != nil {
29+
return err
30+
}
31+
}
32+
return nil
33+
}
34+
35+
// ConsoleNotifier prints logs on console.
36+
type ConsoleNotifier struct {
37+
}
38+
39+
func NewConsoleNotifier() *ConsoleNotifier {
40+
return &ConsoleNotifier{}
41+
}
42+
func (c *ConsoleNotifier) Notify(title, message string) error {
43+
_, err := fmt.Printf("[%s] %s\n", title, message)
44+
return err
45+
}
46+
47+
// DesktopNotifier sends desktop notifications using beeep.
48+
type DesktopNotifier struct {
49+
IconPath string
50+
NotificationTitle string
51+
}
52+
53+
func NewDesktopNotofier(NotificationTitle, iconPath string) *DesktopNotifier {
54+
return &DesktopNotifier{
55+
IconPath: iconPath,
56+
NotificationTitle: NotificationTitle,
57+
}
58+
}
59+
60+
func (d *DesktopNotifier) Notify(title, message string) error {
61+
if beeep.AppName != d.NotificationTitle {
62+
beeep.AppName = d.NotificationTitle
63+
}
64+
if err := beeep.Notify(title, message, d.IconPath); err != nil {
65+
return fmt.Errorf("desktop notification error: %v", err)
66+
}
67+
return nil
68+
}
69+
70+
// Future backends:
71+
//
72+
// type EmailNotifier struct{}
73+
// func (e *EmailNotifier) Notify(title, message string) error { ... }
74+
//
75+
// type TelegramNotifier struct{}
76+
// func (s *SlackNotifier) Notify(title, message string) error { ... }

0 commit comments

Comments
 (0)