| // alertmanager parses the JSON alerts sent from the Prometheus AlertManager |
| // and produces formatted emails from the data. |
| package alertmanager |
| |
| import ( |
| "bytes" |
| "encoding/json" |
| "fmt" |
| "html/template" |
| "io" |
| "strings" |
| "time" |
| |
| "go.skia.org/infra/go/sklog" |
| ) |
| |
| const ( |
| alert_email = `<b>Alerts</b>: {{range .GroupLabels}}{{.}}{{end}} <br><br> |
| |
| <table border="0" cellspacing="5" cellpadding="5"> |
| <tr> |
| <th>Name</th> |
| <th>Severity</th> |
| <th>Status</th></th> |
| <th>Description</th> |
| </tr> |
| {{range .Alerts}} |
| <tr> |
| <td>{{.Labels.alertname}}</td> |
| <td>{{.Labels.severity}}</td> |
| <td>{{.Status}}</td> |
| <td>{{.Annotations.description}}</td> |
| </tr> |
| {{end}} |
| </table> |
| ` |
| |
| alert_chat = `*{{range .GroupLabels}}{{.}}{{end}}*{{ $length := len .Alerts}}{{with index .Alerts 0}}{{ if eq $length 1 }} |
| *{{.Status}}* ({{.Labels.severity}}) {{.Annotations.description}} |
| {{ else }} [{{$length}}] |
| |
| *{{.Status}}* {{.Annotations.description}} |
| |
| {{end}} {{end}} {{ if ne $length 1 }} {{range .Alerts}}{{.Annotations.abbr}} {{end}} |
| {{end}}` |
| ) |
| |
| var ( |
| emailTemplate = template.Must(template.New("alert_email").Parse(alert_email)) |
| chatTemplate = template.Must(template.New("alert_chat").Parse(alert_chat)) |
| loc *time.Location |
| ) |
| |
| func init() { |
| var err error |
| loc, err = time.LoadLocation("America/New_York") |
| if err != nil { |
| sklog.Errorf("Failed to load time location: %s", err) |
| } |
| } |
| |
| // AlertManagerRequest is used to parse the incoming JSON. |
| type AlertManagerRequest struct { |
| Receiver string `json:"receiver"` |
| Status string `json:"status"` |
| Alerts []*Alert `json:"alerts"` |
| |
| GroupLabels map[string]string `json:"groupLabels"` |
| CommonLabels map[string]string `json:"commonLabels"` |
| CommonAnnotations map[string]string `json:"commonAnnotations"` |
| |
| ExternalURL string `json:"externalURL"` |
| } |
| |
| // Alert is used in AlertManagerRequest. |
| type Alert struct { |
| Status string `json:"status"` |
| Labels map[string]string `json:"labels"` |
| Annotations map[string]string `json:"annotations"` |
| StartsAt time.Time `json:"startsAt"` |
| // TODO format is wrong, actually sent as: "startsAt": "2017-01-05T15:21:52.805-05:00" |
| EndsAt time.Time `json:"endsAt"` |
| GeneratorURL string `json:"generatorURL"` |
| } |
| |
| // caps fixes capitalization in some fields. |
| func caps(a *AlertManagerRequest) *AlertManagerRequest { |
| a.Status = strings.Title(a.Status) |
| for _, alert := range a.Alerts { |
| alert.Status = strings.Title(alert.Status) |
| if severity, ok := alert.Labels["severity"]; ok { |
| alert.Labels["severity"] = strings.Title(severity) |
| } |
| } |
| return a |
| } |
| |
| func extractRequest(r io.Reader) (*AlertManagerRequest, []string, time.Time, error) { |
| request := &AlertManagerRequest{} |
| if err := json.NewDecoder(r).Decode(request); err != nil { |
| return nil, nil, time.Time{}, fmt.Errorf("Failed to decode incoming AlertManagerRequest: %s", err) |
| } |
| sklog.Infof("AlertManagerRequest: %v", *request) |
| for _, a := range request.Alerts { |
| sklog.Infof("Alert: %v", *a) |
| } |
| |
| startTime := time.Now() |
| alertnames := []string{} |
| for _, alert := range request.Alerts { |
| alertnames = append(alertnames, alert.Labels["alertname"]) |
| if alert.StartsAt.Before(startTime) { |
| startTime = alert.StartsAt |
| } |
| } |
| if loc != nil { |
| startTime = startTime.In(loc) |
| } |
| return caps(request), alertnames, startTime, nil |
| } |
| |
| // Email returns the body and subject of an email to send for the given alerts. |
| func Email(r io.Reader) (string, string, error) { |
| request, alertnames, startTime, err := extractRequest(r) |
| if err != nil { |
| return "", "", fmt.Errorf("Failed to extract request from JSON: %s", err) |
| } |
| |
| subject := fmt.Sprintf("Alert: %s started at %s", strings.Join(alertnames, " "), startTime.Format("3:04pm MST (2 Jan 2006)")) |
| var b bytes.Buffer |
| if err := emailTemplate.Execute(&b, request); err != nil { |
| return "", "", fmt.Errorf("Failed to template alert: %s", err) |
| } |
| return b.String(), subject, nil |
| } |
| |
| // Chat returns the body of a chat message to send for the given alerts. |
| func Chat(r io.Reader) (string, error) { |
| request, _, _, err := extractRequest(r) |
| if err != nil { |
| return "", fmt.Errorf("Failed to extract request from JSON: %s", err) |
| } |
| |
| var b bytes.Buffer |
| if err := chatTemplate.Execute(&b, request); err != nil { |
| return "", fmt.Errorf("Failed to template alert: %s", err) |
| } |
| return b.String(), nil |
| } |