Skip to content

Commit 2b66df5

Browse files
authored
Merge pull request #4 from SoulKyu/integration/multiple-alertmanagers
Integration/multiple alertmanagers
2 parents 158ed32 + d85f8f0 commit 2b66df5

41 files changed

Lines changed: 12874 additions & 1141 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ go.work.sum
3333
# .vscode/
3434
notificator
3535
fyne-cross
36-
*.tar.*
36+
*.tar.*
37+
notificator.db
38+
internal_doc
39+
fake_alertmanager.log

README.md

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Notificator
22

3-
A GUI application for Alertmanager with sound and notification alerts on your laptop.
3+
A GUI application for multiple Alertmanagers with sound and notification alerts on your laptop.
44

55
![alt text](img/preview.gif "Preview")
66

@@ -37,16 +37,25 @@ Notificator uses a JSON configuration file located at `~/.config/notificator/con
3737

3838
```json
3939
{
40-
"alertmanager": {
41-
"url": "http://localhost:9093",
42-
"username": "",
43-
"password": "",
44-
"token": "",
45-
"headers": {},
46-
"oauth": {
47-
"enabled": false,
48-
"proxy_mode": true
40+
"alertmanagers": [
41+
{
42+
"name": "default",
43+
"url": "http://localhost:9093",
44+
"username": "",
45+
"password": "",
46+
"token": "",
47+
"headers": {},
48+
"oauth": {
49+
"enabled": false,
50+
"proxy_mode": true
51+
}
4952
}
53+
],
54+
"backend": {
55+
"enabled": true,
56+
"grpc_listen": ":50051",
57+
"grpc_client": "localhost:50051",
58+
"http_listen": ":8080"
5059
},
5160
"gui": {
5261
"width": 1200,
@@ -78,6 +87,10 @@ Notificator uses a JSON configuration file located at `~/.config/notificator/con
7887

7988
- **alertmanager.url**: Alertmanager API endpoint
8089
- **alertmanager.headers**: Custom HTTP headers for authentication
90+
- **backend.enabled**: Enable/disable backend collaboration features
91+
- **backend.grpc_listen**: Port for gRPC server to listen on (e.g., ":50051")
92+
- **backend.grpc_client**: Address for gRPC client connections (e.g., "localhost:50051")
93+
- **backend.http_listen**: Port for HTTP server (health checks, metrics) (e.g., ":8080")
8194
- **notifications.enabled**: Enable/disable all notifications
8295
- **notifications.sound_enabled**: Enable/disable sound alerts
8396
- **notifications.critical_only**: Only notify for critical alerts
@@ -87,6 +100,7 @@ Notificator uses a JSON configuration file located at `~/.config/notificator/con
87100
### Environment Variables
88101

89102
You can also set headers via environment variable:
103+
90104
```bash
91105
export METRICS_PROVIDER_HEADERS="X-API-Key=your-key,Authorization=Bearer token"
92106
```
@@ -100,5 +114,6 @@ export METRICS_PROVIDER_HEADERS="X-API-Key=your-key,Authorization=Bearer token"
100114
- Alert silencing functionality
101115
- Customizable notification settings
102116
- Light/dark theme support
117+
- Multiple Alertmanagers
103118

104119
Perfect for keeping track of your infrastructure alerts directly from your laptop!

alertmanager/fake/fake_alertmanager.py

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -143,8 +143,12 @@ def create_alert_groups():
143143
alert_groups = list(groups.values())
144144

145145
def alert_generator():
146-
"""Background thread to generate random alerts"""
146+
"""Background thread to generate random alerts and resolve them periodically"""
147+
last_resolution_time = datetime.now(UTC)
148+
147149
while True:
150+
current_time = datetime.now(UTC)
151+
148152
# Generate new alerts randomly
149153
if random.random() < 0.4: # 40% chance every 15 seconds
150154
new_alert = generate_random_alert()
@@ -163,8 +167,29 @@ def alert_generator():
163167
if alert["status"]["state"] == "unprocessed":
164168
alert["status"]["state"] = "active"
165169

166-
# Remove some old alerts randomly
167-
if alerts and random.random() < 0.15: # 15% chance to remove
170+
# Resolve alerts every 30 seconds (for testing resolved alerts feature)
171+
if (current_time - last_resolution_time).total_seconds() >= 30:
172+
print(f"[{current_time.strftime('%H:%M:%S')}] Resolving alerts for testing...", flush=True)
173+
174+
# Resolve 30-50% of active alerts
175+
active_alerts = [alert for alert in alerts if alert["status"]["state"] == "active"]
176+
if active_alerts:
177+
resolve_count = max(1, int(len(active_alerts) * random.uniform(0.3, 0.5)))
178+
alerts_to_resolve = random.sample(active_alerts, min(resolve_count, len(active_alerts)))
179+
180+
for alert in alerts_to_resolve:
181+
alerts.remove(alert)
182+
print(f" Resolved: {alert['labels']['alertname']} on {alert['labels']['instance']}", flush=True)
183+
184+
create_alert_groups()
185+
print(f" Total resolved: {len(alerts_to_resolve)} alerts", flush=True)
186+
else:
187+
print(" No active alerts to resolve", flush=True)
188+
189+
last_resolution_time = current_time
190+
191+
# Remove some old alerts randomly (less frequently now)
192+
if alerts and random.random() < 0.05: # 5% chance to remove
168193
alerts.pop(0)
169194
create_alert_groups()
170195

config/config.go

Lines changed: 166 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ import (
1111

1212
// Config holds all configuration for the application
1313
type Config struct {
14-
// Alertmanager configuration
15-
Alertmanager AlertmanagerConfig `json:"alertmanager"`
14+
// Alertmanager configurations, supports multiple instances
15+
Alertmanagers []AlertmanagerConfig `json:"alertmanagers"`
1616

1717
// GUI configuration
1818
GUI GUIConfig `json:"gui"`
@@ -24,11 +24,40 @@ type Config struct {
2424
Polling PollingConfig `json:"polling"`
2525

2626
// Column configuration for GUI
27-
ColumnWidths map[string]float32 `json:"column_widths"`
27+
ColumnWidths map[string]float32 `json:"column_widths"`
28+
Backend BackendConfig `json:"backend"`
29+
ResolvedAlerts ResolvedAlertsConfig `json:"resolved_alerts"`
30+
}
31+
32+
type BackendConfig struct {
33+
Enabled bool `json:"enabled"`
34+
GRPCListen string `json:"grpc_listen"` // Port for gRPC server (e.g., ":50051")
35+
GRPCClient string `json:"grpc_client"` // Address for gRPC client (e.g., "localhost:50051")
36+
HTTPListen string `json:"http_listen"` // Port for HTTP server (e.g., ":8080")
37+
Database DatabaseConfig `json:"database"`
38+
}
39+
40+
type DatabaseConfig struct {
41+
Type string `json:"type"` // "sqlite" or "postgres"
42+
Host string `json:"host"`
43+
Port int `json:"port"`
44+
Name string `json:"name"`
45+
User string `json:"user"`
46+
Password string `json:"password"`
47+
SSLMode string `json:"ssl_mode"`
48+
SQLitePath string `json:"sqlite_path"`
49+
}
50+
51+
// ResolvedAlertsConfig contains resolved alerts settings
52+
type ResolvedAlertsConfig struct {
53+
Enabled bool `json:"enabled"` // Enable resolved alerts tracking
54+
NotificationsEnabled bool `json:"notifications_enabled"` // Send notifications for resolved alerts
55+
RetentionDuration time.Duration `json:"retention_duration"` // How long to keep resolved alerts
2856
}
2957

3058
// AlertmanagerConfig contains Alertmanager-specific settings
3159
type AlertmanagerConfig struct {
60+
Name string `json:"name"`
3261
URL string `json:"url"`
3362
Username string `json:"username"`
3463
Password string `json:"password"`
@@ -57,10 +86,13 @@ type GUIConfig struct {
5786

5887
// FilterStateConfig contains the state of filters
5988
type FilterStateConfig struct {
60-
SearchText string `json:"search_text"`
61-
SelectedSeverities map[string]bool `json:"selected_severities"`
62-
SelectedStatuses map[string]bool `json:"selected_statuses"`
63-
SelectedTeams map[string]bool `json:"selected_teams"`
89+
SearchText string `json:"search_text"`
90+
SelectedAlertmanagers map[string]bool `json:"selected_alertmanagers"`
91+
SelectedSeverities map[string]bool `json:"selected_severities"`
92+
SelectedStatuses map[string]bool `json:"selected_statuses"`
93+
SelectedTeams map[string]bool `json:"selected_teams"`
94+
SelectedAcks map[string]bool `json:"selected_acks"`
95+
SelectedComments map[string]bool `json:"selected_comments"`
6496
}
6597

6698
// NotificationConfig contains notification settings
@@ -91,10 +123,13 @@ func DefaultConfig() *Config {
91123
}
92124

93125
return &Config{
94-
Alertmanager: AlertmanagerConfig{
95-
URL: "http://localhost:9093",
96-
Headers: headers,
97-
OAuth: oauthConfig,
126+
Alertmanagers: []AlertmanagerConfig{
127+
{
128+
Name: "Default",
129+
URL: "http://localhost:9093",
130+
Headers: headers,
131+
OAuth: oauthConfig,
132+
},
98133
},
99134
GUI: GUIConfig{
100135
Width: 1920,
@@ -105,10 +140,13 @@ func DefaultConfig() *Config {
105140
ShowTrayIcon: true,
106141
BackgroundMode: false,
107142
FilterState: FilterStateConfig{
108-
SearchText: "",
109-
SelectedSeverities: map[string]bool{"All": true},
110-
SelectedStatuses: map[string]bool{"All": true},
111-
SelectedTeams: map[string]bool{"All": true},
143+
SearchText: "",
144+
SelectedAlertmanagers: map[string]bool{"All": true},
145+
SelectedSeverities: map[string]bool{"All": true},
146+
SelectedStatuses: map[string]bool{"All": true},
147+
SelectedTeams: map[string]bool{"All": true},
148+
SelectedAcks: map[string]bool{"All": true},
149+
SelectedComments: map[string]bool{"All": true},
112150
},
113151
},
114152
Notifications: NotificationConfig{
@@ -131,6 +169,27 @@ func DefaultConfig() *Config {
131169
Polling: PollingConfig{
132170
Interval: 30 * time.Second,
133171
},
172+
Backend: BackendConfig{
173+
Enabled: false,
174+
GRPCListen: ":50051",
175+
GRPCClient: "localhost:50051",
176+
HTTPListen: ":8080",
177+
Database: DatabaseConfig{
178+
Type: "sqlite",
179+
SQLitePath: "./notificator.db",
180+
Host: "localhost",
181+
Port: 5432,
182+
Name: "notificator",
183+
User: "notificator",
184+
Password: "",
185+
SSLMode: "disable",
186+
},
187+
},
188+
ResolvedAlerts: ResolvedAlertsConfig{
189+
Enabled: true, // Enable by default
190+
NotificationsEnabled: true, // Send notifications by default
191+
RetentionDuration: 1 * time.Hour, // Keep for 1 hour by default
192+
},
134193
}
135194
}
136195

@@ -179,6 +238,12 @@ func LoadConfig(configPath string) (*Config, error) {
179238
if config.GUI.FilterState.SelectedTeams == nil {
180239
config.GUI.FilterState.SelectedTeams = map[string]bool{"All": true}
181240
}
241+
if config.GUI.FilterState.SelectedAcks == nil {
242+
config.GUI.FilterState.SelectedAcks = map[string]bool{"All": true}
243+
}
244+
if config.GUI.FilterState.SelectedComments == nil {
245+
config.GUI.FilterState.SelectedComments = map[string]bool{"All": true}
246+
}
182247

183248
return &config, nil
184249
}
@@ -242,11 +307,93 @@ func ParseHeadersFromEnv(envVar string) map[string]string {
242307
func (c *Config) MergeHeaders() {
243308
envHeaders := ParseHeadersFromEnv("METRICS_PROVIDER_HEADERS")
244309

245-
if c.Alertmanager.Headers == nil {
246-
c.Alertmanager.Headers = make(map[string]string)
310+
// Apply environment headers to all alertmanagers
311+
for i := range c.Alertmanagers {
312+
if c.Alertmanagers[i].Headers == nil {
313+
c.Alertmanagers[i].Headers = make(map[string]string)
314+
}
315+
316+
for key, value := range envHeaders {
317+
c.Alertmanagers[i].Headers[key] = value
318+
}
247319
}
320+
}
248321

249-
for key, value := range envHeaders {
250-
c.Alertmanager.Headers[key] = value
322+
// GetAlertmanagerByName returns an Alertmanager configuration by name
323+
func (c *Config) GetAlertmanagerByName(name string) *AlertmanagerConfig {
324+
for i := range c.Alertmanagers {
325+
if c.Alertmanagers[i].Name == name {
326+
return &c.Alertmanagers[i]
327+
}
251328
}
329+
return nil
330+
}
331+
332+
// GetAlertmanagerByURL returns an Alertmanager configuration by URL
333+
func (c *Config) GetAlertmanagerByURL(url string) *AlertmanagerConfig {
334+
for i := range c.Alertmanagers {
335+
if c.Alertmanagers[i].URL == url {
336+
return &c.Alertmanagers[i]
337+
}
338+
}
339+
return nil
340+
}
341+
342+
// AddAlertmanager adds a new Alertmanager configuration
343+
func (c *Config) AddAlertmanager(config AlertmanagerConfig) {
344+
// Ensure unique name
345+
if c.GetAlertmanagerByName(config.Name) != nil {
346+
// Find a unique name
347+
baseName := config.Name
348+
counter := 1
349+
for c.GetAlertmanagerByName(config.Name) != nil {
350+
config.Name = fmt.Sprintf("%s_%d", baseName, counter)
351+
counter++
352+
}
353+
}
354+
c.Alertmanagers = append(c.Alertmanagers, config)
355+
}
356+
357+
// RemoveAlertmanager removes an Alertmanager configuration by name
358+
func (c *Config) RemoveAlertmanager(name string) bool {
359+
for i := range c.Alertmanagers {
360+
if c.Alertmanagers[i].Name == name {
361+
c.Alertmanagers = append(c.Alertmanagers[:i], c.Alertmanagers[i+1:]...)
362+
return true
363+
}
364+
}
365+
return false
366+
}
367+
368+
// GetAlertmanagerNames returns a list of all Alertmanager names
369+
func (c *Config) GetAlertmanagerNames() []string {
370+
names := make([]string, len(c.Alertmanagers))
371+
for i, am := range c.Alertmanagers {
372+
names[i] = am.Name
373+
}
374+
return names
375+
}
376+
377+
// ValidateAlertmanagers validates all Alertmanager configurations
378+
func (c *Config) ValidateAlertmanagers() error {
379+
if len(c.Alertmanagers) == 0 {
380+
return fmt.Errorf("at least one Alertmanager must be configured")
381+
}
382+
383+
names := make(map[string]bool)
384+
urls := make(map[string]bool)
385+
386+
for i, am := range c.Alertmanagers {
387+
if am.Name == "" {
388+
return fmt.Errorf("alertmanager at index %d has no name", i)
389+
}
390+
names[am.Name] = true
391+
392+
if am.URL == "" {
393+
return fmt.Errorf("alertmanager '%s' has no URL", am.Name)
394+
}
395+
urls[am.URL] = true
396+
}
397+
398+
return nil
252399
}

0 commit comments

Comments
 (0)