-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.go
355 lines (317 loc) · 9.85 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
package main
import (
// "database/sql"
"fmt"
"io/ioutil"
"log"
// "math"
// "os"
// "os/signal"
"strings"
// "syscall"
"encoding/json"
"github.com/go-yaml/yaml"
"github.com/thoj/go-ircevent"
"net/http"
// "strconv"
"time"
)
type Config struct {
Server string `yaml:"server"`
Username string `yaml:"username"`
Password string `yaml:"password"`
Autojoin string `yaml:"autojoin"`
AlertsChannel string `yaml:"alertsChannel"`
DatabaseURL string `yaml:"DatabaseURL"`
DBusername string `yaml:"DBusername"`
DBpassword string `yaml:"DBpassword"`
WebAddress string `yaml:"webAddress"`
}
type Bot struct {
conn *irc.Connection
config *Config
alerts map[string]AlertRecord
// db *sql.DB
startTime time.Time // needed for alert timedeltas.
}
type AlertRecord struct {
Name string
Rate_limit time.Duration
Mute_until time.Time
Last_seen time.Time
Last_reported time.Time
}
////////////////////////////////////////
// IRC stuff
func NewBot(config *Config) (*Bot, error) {
// build a new IRC bot object(from go-ircevent), given a config
conn := irc.IRC(config.Username, config.Username)
// conn.VerboseCallbackHandler = true
// conn.Debug = true
conn.Password = config.Password
conn.SASLLogin = config.Username
conn.SASLPassword = config.Password
bot := &Bot{
conn: conn,
config: config, // is this config the right format?
alerts: make(map[string]AlertRecord),
startTime: time.Now(),
}
return bot, nil
}
// NOTE: func [class] functionName([arguments]) ([returns])
func (b *Bot) Connect() error {
return b.conn.Connect(b.config.Server)
}
func (b *Bot) PrivmsgCallback(event *irc.Event) {
if event.Nick != "DaPinkOne" {
// TODO: auth function.
return
}
// if user is auth'd:
fmt.Printf("Command recieved from %s on channel %s for: %s",
event.Nick,
event.Arguments[0],
event.Arguments[1],
)
fields := strings.Fields(event.Arguments[1])
b.conn.Privmsg(event.Arguments[0], "Acknowledged: "+fields[0])
switch fields[0] { // switch on command.
case "quit":
b.conn.Quit()
case "join":
if len(fields) >= 1 {
b.conn.Join(fields[1])
}
case "part":
if len(fields) >= 1 { // error message?
b.conn.Part(fields[1])
return
}
// default to parting current channel, if one isn't given.
b.conn.Part(event.Arguments[0])
case "alert": // list alerts
switch fields[1] { // list commands
case "mute":
// `alert mute xyz [43d23h8m]` NOTE: does not currently support days.
// when told to mute an alert, mute for a period
// if period not given, default to max unix timestamp.
alert_name := fields[2]
var mute_delta time.Duration // TODO: default value for mute duration?
var err error
if len(fields) > 3 {
mute_delta, err = time.ParseDuration(fields[3])
if err != nil {
b.conn.Privmsg(event.Arguments[0], "Invalid time format: "+fields[3])
return
}
}
mute_until := time.Now().Add(mute_delta)
b.conn.Privmsg(event.Arguments[0],
fmt.Sprintf("Muting alert `%s` until %s",
alert_name,
mute_until.String(),
),
)
val := b.alerts[alert_name]
b.alerts[alert_name] = AlertRecord{
Name: alert_name,
Mute_until: mute_until,
Last_seen: val.Last_seen,
Rate_limit: val.Rate_limit,
}
case "rate": // set alert rate limit
alert_name := fields[2]
var rate_limit time.Duration
var err error
if len(fields) > 3 {
rate_limit, err = time.ParseDuration(fields[3])
if err != nil {
b.conn.Privmsg(event.Arguments[0], "Invalid time format: "+fields[3])
return
}
} else {
b.conn.Privmsg(event.Arguments[0], "Time delta required for rate limit. ")
return
}
log.Printf("%s limited to %d\n", alert_name, rate_limit.String())
val := b.alerts[alert_name]
b.alerts[alert_name] = AlertRecord{
Name: alert_name,
Mute_until: val.Mute_until,
Last_seen: val.Last_seen,
Rate_limit: rate_limit,
Last_reported: val.Last_reported,
}
case "unmute": // unmute by setting mute_until to current time.
alert_name := fields[2]
val := b.alerts[alert_name]
b.alerts[alert_name] = AlertRecord{
Name: alert_name,
Mute_until: time.Now(),
Last_seen: val.Last_seen,
Rate_limit: val.Rate_limit,
}
case "list": // alerts list
lst := make([]string, len(b.alerts))
for k, _ := range b.alerts { // map() ?
lst = append(lst, k)
}
b.conn.Privmsg(event.Arguments[0], "Alerts:"+strings.Join(lst, " "))
case "info": // get information about an alert record.
alert_name := fields[2]
record, ok := b.alerts[alert_name]
var msg string
if ok {
msg = fmt.Sprintf("`%s` : mute %s : seen %s : rate %s : reported %s",
alert_name,
record.Mute_until.String(),
record.Last_seen.String(),
record.Rate_limit.String(),
record.Last_reported.String(),
)
} else {
msg = fmt.Sprintf("Alert `%s` has no record.", alert_name)
}
b.conn.Privmsg(event.Arguments[0], msg)
default:
// default case? reply w/ error message?
}
}
}
// End of IRC stuff
///////////////////////////
//////////////////////////
// web stuff
type HttpAlert struct {
Receiver string `json:"receiver"`
Status string `json:"status"`
Alerts []InnerAlert `json:"alerts"`
GroupLabels map[string]string `json:"groupLabels"`
CommonLabels map[string]string `json:"commonLabels"`
CommonAnnotations map[string]string `json:"commonAnnotations"`
ExternalURL string `json:"externalURL"`
Version string `json:"version"`
GroupKey string `json:"groupKey"`
TruncatedAlerts int `json:"truncatedAlerts"`
OrgID int `json:"orgId"`
Title string `json:"title"`
State string `json:"state"`
Message string `json:"message"`
}
type InnerAlert struct {
Status string `json:"status"`
Labels map[string]string `json:"labels"`
Annotations map[string]string `json:"annotations"`
StartsAt string `json:"startsAt"`
EndsAt string `json:"endsAt"`
GeneratorURL string `json:"generatorURL"`
Fingerprint string `json:"fingerprint"`
SilenceURL string `json:"silenceURL"`
DashboardURL string `json:"dashboardURL"`
PanelURL string `json:"panelURL"`
Values interface{} `json:"values"` // TODO: what type is "Values" ?
ValueString string `json:"valueString"`
}
func buildAlertHandler(alertsChannel chan<- InnerAlert) func(w http.ResponseWriter, r *http.Request) {
// takes a channel, and builds a callback which knows about that channel,
// so the callback can send alert POST data recieved to the channel.
return func(w http.ResponseWriter, r *http.Request) {
// callback for http server to handle alerts recieved via post request,
// from grafana. Takes data, and sends necessary information into a channel
// for the IRC bot to report.
var httpAlert HttpAlert
err := json.NewDecoder(r.Body).Decode(&httpAlert) // unmarshal data into struct for use.
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// for each alert in the list given in the POST, if it's firing, send it to the monitor.
for _, innerAlert := range httpAlert.Alerts {
name := innerAlert.Labels["alertname"]
if innerAlert.Status == "firing" {
log.Println("Alert firing: ", name)
alertsChannel <- innerAlert
}
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("Alert recieved."))
}
}
// End web stuff
/////////////////////////////////
func main() {
// read yaml config file
yamlData, err := ioutil.ReadFile("config.yaml")
if err != nil {
fmt.Printf("Err %s", err)
return
}
config := Config{}
err = yaml.Unmarshal(yamlData, &config)
if err != nil {
fmt.Printf("Err %s", err)
return
}
// build new bot
b, err := NewBot(&config)
if err != nil {
fmt.Printf("Err %s", err)
return
}
err = b.Connect() // connect to IRC server.
if err != nil {
fmt.Printf("Err %s", err)
return
}
// TODO: get necessary alert data from database here.
// b.alerts["testAlert"] = Alert{
// Name: "testAlert",
// }
alertsChannel := make(chan InnerAlert)
// register IRC callbacks
b.conn.AddCallback("PRIVMSG", func(event *irc.Event) {
go b.PrivmsgCallback(event)
})
go func() { // set up a monitor to feed data from the channel to IRC.
// monitor will handle rate limiting, muting alerts, etc.
for {
innerAlert := <-alertsChannel
now := time.Now()
alertName := innerAlert.Labels["alertname"]
record := b.alerts[alertName]
log.Println(record)
// if the innerAlert is not muted, and is not within the rate limit, report.
report := true
if now.Before(record.Mute_until) || now.Before(record.Last_reported.Add(record.Rate_limit)) {
// TODO: need not only last_seen, but also Last_Reported for use in rate limit.
log.Println("Alert muted/limited, ", alertName)
report = false
}
last_reported := record.Last_reported
if report {
last_reported = now
b.conn.Privmsg(b.config.AlertsChannel, alertName+" is "+innerAlert.Status)
}
b.alerts[alertName] = AlertRecord{
Name: alertName,
Last_seen: now,
Rate_limit: record.Rate_limit,
Mute_until: record.Mute_until,
Last_reported: last_reported,
}
}
}()
fmt.Printf("Connected to IRCServer: %s\n", config.Server)
fmt.Printf("Connected to IRC w/ Username: %s\n", config.Username)
b.conn.Join(b.config.Autojoin)
// register `handleAlert` as a callback with the http server.
http.HandleFunc("/alerts", buildAlertHandler(alertsChannel))
webAddr := b.config.WebAddress
go func() { // start web server loop.
log.Fatal(http.ListenAndServe(webAddr, nil))
}()
log.Println("Web Server started on", webAddr)
// start IRC bot main loop
b.conn.Loop()
}