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  }