bosun.org@v0.0.0-20210513094433-e25bc3e69a1f/cmd/bosun/conf/notify.go (about) 1 package conf 2 3 import ( 4 "bytes" 5 "crypto/tls" 6 "errors" 7 "fmt" 8 "io" 9 "io/ioutil" 10 "net/http" 11 "net/mail" 12 "net/smtp" 13 "strings" 14 15 "bosun.org/collect" 16 "bosun.org/metadata" 17 "bosun.org/models" 18 "bosun.org/slog" 19 "bosun.org/util" 20 "github.com/jordan-wright/email" 21 ) 22 23 const ( 24 sendLogSuccessFmt = "%s; name: %s; transport: %s; dst: %s; body: %s" 25 sendLogErrorFmt = "%s; name: %s; transport: %s; dst: %s; body: %s; error: %s" 26 httpSendErrorFmt = "bad response for '%s' %s notification using template key '%s' for alert keys %v method %s: %d" 27 ) 28 29 func init() { 30 metadata.AddMetricMeta( 31 "bosun.email.sent", metadata.Counter, metadata.PerSecond, 32 "The number of email notifications sent by Bosun.") 33 metadata.AddMetricMeta( 34 "bosun.email.sent_failed", metadata.Counter, metadata.PerSecond, 35 "The number of email notifications that Bosun failed to send.") 36 metadata.AddMetricMeta( 37 "bosun.post.sent", metadata.Counter, metadata.PerSecond, 38 "The number of post notifications sent by Bosun.") 39 metadata.AddMetricMeta( 40 "bosun.post.sent_failed", metadata.Counter, metadata.PerSecond, 41 "The number of post notifications that Bosun failed to send.") 42 } 43 44 type PreparedNotifications struct { 45 Email *PreparedEmail 46 HTTP []*PreparedHttp 47 Print bool 48 Name string 49 Errors []string 50 } 51 52 func (p *PreparedNotifications) Send(c SystemConfProvider) (errs []error) { 53 if p.Email != nil { 54 if err := p.Email.Send(c); err != nil { 55 slog.Errorf( 56 sendLogErrorFmt, 57 fmt.Sprintf("subject: %s", p.Email.Subject), 58 p.Name, 59 "email", 60 strings.Join(p.Email.To, ","), 61 p.Email.Body, 62 err.Error(), 63 ) 64 errs = append(errs, err) 65 } else if p.Print { 66 slog.Infof( 67 sendLogSuccessFmt, 68 fmt.Sprintf("subject: %s", p.Email.Subject), 69 p.Name, 70 "email", 71 strings.Join(p.Email.To, ","), 72 p.Email.Body, 73 ) 74 } 75 } 76 for _, h := range p.HTTP { 77 var logPrefix string 78 if h.Details.At != "" { 79 logPrefix = fmt.Sprintf("action_type: %s", h.Details.At) 80 } else { 81 logPrefix = "type: alert" 82 } 83 if _, err := h.Send(); err != nil { 84 slog.Errorf( 85 sendLogErrorFmt, 86 logPrefix, 87 h.Details.NotifyName, 88 "http_"+h.Method, 89 h.URL, 90 h.Body, 91 err.Error(), 92 ) 93 errs = append(errs, err) 94 } else if p.Print { 95 slog.Infof( 96 sendLogSuccessFmt, 97 logPrefix, 98 h.Details.NotifyName, 99 "http_"+h.Method, 100 h.URL, 101 h.Body, 102 ) 103 } 104 } 105 106 return 107 } 108 109 // PrepareAlert does all of the work of selecting what content to send to which sources. It does not actually send any notifications, 110 // but the returned object can be used to send them. 111 func (n *Notification) PrepareAlert(rt *models.RenderedTemplates, ak string, attachments ...*models.Attachment) *PreparedNotifications { 112 pn := &PreparedNotifications{Name: n.Name, Print: n.Print} 113 if len(n.Email) > 0 { 114 subject := rt.GetDefault(n.EmailSubjectTemplate, "emailSubject") 115 body := rt.GetDefault(n.BodyTemplate, "emailBody") 116 pn.Email = n.PrepEmail(subject, body, ak, attachments) 117 } 118 if n.Post != nil || n.PostTemplate != "" { 119 url := "" 120 if n.Post != nil { 121 url = n.Post.String() 122 } else { 123 url = rt.Get(n.PostTemplate) 124 } 125 body := rt.GetDefault(n.BodyTemplate, "subject") 126 details := &NotificationDetails{ 127 Ak: []string{ak}, 128 NotifyName: n.Name, 129 TemplateKey: n.BodyTemplate, 130 NotifyType: 1, 131 } 132 pn.HTTP = append(pn.HTTP, n.PrepHttp("POST", url, body, details)) 133 } 134 if n.Get != nil || n.GetTemplate != "" { 135 url := "" 136 if n.Get != nil { 137 url = n.Get.String() 138 } else { 139 url = rt.Get(n.GetTemplate) 140 } 141 details := &NotificationDetails{ 142 Ak: []string{ak}, 143 NotifyName: n.Name, 144 TemplateKey: n.BodyTemplate, 145 NotifyType: 1, 146 } 147 pn.HTTP = append(pn.HTTP, n.PrepHttp("GET", url, "", details)) 148 } 149 return pn 150 } 151 152 // NotifyAlert triggers Email/HTTP/Print actions for the Notification object. Called when an alert is first triggered, or on escalations. 153 func (n *Notification) NotifyAlert(rt *models.RenderedTemplates, c SystemConfProvider, ak string, attachments ...*models.Attachment) { 154 go n.PrepareAlert(rt, ak, attachments...).Send(c) 155 } 156 157 type PreparedHttp struct { 158 URL string 159 Method string 160 Headers map[string]string `json:",omitempty"` 161 Body string 162 Details *NotificationDetails 163 } 164 165 const ( 166 alert = iota + 1 167 unknown 168 multiunknown 169 ) 170 171 type NotificationDetails struct { 172 Ak []string // alert key 173 At string // action type 174 NotifyName string // notification name 175 TemplateKey string // template key 176 NotifyType int // notifications type e.g alert, unknown etc 177 } 178 179 func (p *PreparedHttp) Send() (int, error) { 180 var body io.Reader 181 if p.Body != "" { 182 body = strings.NewReader(p.Body) 183 } 184 req, err := http.NewRequest(p.Method, p.URL, body) 185 if err != nil { 186 return 0, err 187 } 188 for k, v := range p.Headers { 189 req.Header.Set(k, v) 190 } 191 resp, err := http.DefaultClient.Do(req) 192 if resp != nil && resp.Body != nil { 193 // Drain up to 512 bytes and close the body to let the Transport reuse the connection 194 io.CopyN(ioutil.Discard, resp.Body, 512) 195 resp.Body.Close() 196 } 197 if err != nil { 198 return 0, err 199 } 200 if resp.StatusCode >= 300 { 201 collect.Add("post.sent_failed", nil, 1) 202 switch p.Details.NotifyType { 203 case alert: 204 return resp.StatusCode, fmt.Errorf( 205 httpSendErrorFmt, 206 p.Details.NotifyName, 207 "alert", 208 p.Details.TemplateKey, 209 strings.Join(p.Details.Ak, ","), 210 p.Method, 211 resp.StatusCode, 212 ) 213 case unknown: 214 return resp.StatusCode, fmt.Errorf( 215 httpSendErrorFmt, 216 p.Details.NotifyName, 217 "unknown", 218 p.Details.TemplateKey, 219 strings.Join(p.Details.Ak, ","), 220 p.Method, 221 resp.StatusCode, 222 ) 223 case multiunknown: 224 return resp.StatusCode, fmt.Errorf( 225 httpSendErrorFmt, 226 p.Details.NotifyName, 227 "multi-unknown", 228 p.Details.TemplateKey, 229 strings.Join(p.Details.Ak, ","), 230 p.Method, 231 resp.StatusCode, 232 ) 233 default: 234 return resp.StatusCode, fmt.Errorf( 235 httpSendErrorFmt, 236 p.Details.NotifyName, 237 fmt.Sprintf("action '%s'", p.Details.At), 238 p.Details.TemplateKey, 239 strings.Join(p.Details.Ak, ","), 240 p.Method, 241 resp.StatusCode, 242 ) 243 } 244 } 245 collect.Add("post.sent", nil, 1) 246 return resp.StatusCode, nil 247 } 248 249 func (n *Notification) PrepHttp(method string, url string, body string, alertDetails *NotificationDetails) *PreparedHttp { 250 prep := &PreparedHttp{ 251 Method: method, 252 URL: url, 253 Headers: map[string]string{}, 254 Details: alertDetails, 255 } 256 if method == http.MethodPost { 257 prep.Body = body 258 prep.Headers["Content-Type"] = n.ContentType 259 } 260 return prep 261 } 262 263 func (n *Notification) SendHttp(method string, url string, body string) { 264 details := &NotificationDetails{} 265 p := n.PrepHttp(method, url, body, details) 266 stat, err := p.Send() 267 if err != nil { 268 slog.Errorf("Sending http notification: %s", err) 269 } 270 slog.Infof("%s notification successful for alert %s. Status: %d", method, details.Ak, stat) 271 } 272 273 type PreparedEmail struct { 274 To []string 275 Subject string 276 Body string 277 AK string 278 Attachments []*models.Attachment 279 } 280 281 func (n *Notification) PrepEmail(subject, body string, ak string, attachments []*models.Attachment) *PreparedEmail { 282 pe := &PreparedEmail{ 283 Subject: subject, 284 Body: body, 285 Attachments: attachments, 286 AK: ak, 287 } 288 for _, a := range n.Email { 289 pe.To = append(pe.To, a.Address) 290 } 291 return pe 292 } 293 294 func (p *PreparedEmail) Send(c SystemConfProvider) error { 295 // make sure "To" was not null 296 if len(p.To) <= 0 { 297 return nil 298 } 299 300 e := email.NewEmail() 301 e.From = c.GetEmailFrom() 302 for _, a := range p.To { 303 e.To = append(e.To, a) 304 } 305 e.Subject = p.Subject 306 e.HTML = []byte(p.Body) 307 for _, a := range p.Attachments { 308 e.Attach(bytes.NewBuffer(a.Data), a.Filename, a.ContentType) 309 } 310 e.Headers.Add("X-Bosun-Server", util.GetHostManager().GetHostName()) 311 if err := sendEmail(e, c.GetSMTPHost(), c.GetSMTPUsername(), c.GetSMTPPassword()); err != nil { 312 collect.Add("email.sent_failed", nil, 1) 313 slog.Errorf("failed to send alert %v to %v %v\n", p.AK, e.To, err) 314 return err 315 } 316 collect.Add("email.sent", nil, 1) 317 slog.Infof("relayed email %v to %v sucessfully. Subject: %d bytes. Body: %d bytes.", p.AK, e.To, len(e.Subject), len(e.HTML)) 318 return nil 319 } 320 321 // Send an email using the given host and SMTP auth (optional), returns any 322 // error thrown by smtp.SendMail. This function merges the To, Cc, and Bcc 323 // fields and calls the smtp.SendMail function using the Email.Bytes() output as 324 // the message. 325 func sendEmail(e *email.Email, addr, username, password string) error { 326 // Merge the To, Cc, and Bcc fields 327 to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) 328 to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) 329 // Check to make sure there is at least one recipient and one "From" address 330 if e.From == "" || len(to) == 0 { 331 return errors.New("Must specify at least one From address and one To address") 332 } 333 from, err := mail.ParseAddress(e.From) 334 if err != nil { 335 return err 336 } 337 raw, err := e.Bytes() 338 if err != nil { 339 return err 340 } 341 return smtpSend(addr, username, password, from.Address, to, raw) 342 } 343 344 // SendMail connects to the server at addr, switches to TLS if 345 // possible, authenticates with the optional mechanism a if possible, 346 // and then sends an email from address from, to addresses to, with 347 // message msg. 348 func smtpSend(addr, username, password string, from string, to []string, msg []byte) error { 349 c, err := smtp.Dial(addr) 350 if err != nil { 351 return err 352 } 353 defer c.Close() 354 if err = c.Hello("localhost"); err != nil { 355 return err 356 } 357 if ok, _ := c.Extension("STARTTLS"); ok { 358 if err = c.StartTLS(&tls.Config{InsecureSkipVerify: true}); err != nil { 359 return err 360 } 361 if len(username) > 0 || len(password) > 0 { 362 hostWithoutPort := strings.Split(addr, ":")[0] 363 auth := smtp.PlainAuth("", username, password, hostWithoutPort) 364 if err = c.Auth(auth); err != nil { 365 return err 366 } 367 } 368 } 369 if err = c.Mail(from); err != nil { 370 return err 371 } 372 for _, addr := range to { 373 if err = c.Rcpt(addr); err != nil { 374 return err 375 } 376 } 377 w, err := c.Data() 378 if err != nil { 379 return err 380 } 381 _, err = w.Write(msg) 382 if err != nil { 383 return err 384 } 385 err = w.Close() 386 if err != nil { 387 return err 388 } 389 return c.Quit() 390 }