You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

210 lines
7.2 KiB

  1. // Package alertmanager implements a Service capable of processing webhooks from prometheus alertmanager.
  2. package alertmanager
  3. import (
  4. "bytes"
  5. "encoding/json"
  6. "fmt"
  7. "github.com/matrix-org/go-neb/database"
  8. "github.com/matrix-org/go-neb/types"
  9. "github.com/matrix-org/gomatrix"
  10. log "github.com/sirupsen/logrus"
  11. html "html/template"
  12. "net/http"
  13. "strings"
  14. text "text/template"
  15. )
  16. // ServiceType of the Alertmanager service.
  17. const ServiceType = "alertmanager"
  18. // Service contains the Config fields for the Alertmanager service.
  19. //
  20. // This service will send notifications into a Matrix room when Alertmanager sends
  21. // webhook events to it. It requires a public domain which Alertmanager can reach.
  22. // Notices will be sent as the service user ID.
  23. //
  24. // For the template strings, take a look at https://golang.org/pkg/text/template/
  25. // and the html variant https://golang.org/pkg/html/template/.
  26. // The data they get is a webhookNotification
  27. //
  28. // You can set msg_type to either m.text or m.notice
  29. //
  30. // Example JSON request:
  31. // {
  32. // rooms: {
  33. // "!ewfug483gsfe:localhost": {
  34. // "text_template": "your plain text template goes here",
  35. // "html_template": "your html template goes here",
  36. // "msg_type": "m.text"
  37. // },
  38. // }
  39. // }
  40. type Service struct {
  41. types.DefaultService
  42. webhookEndpointURL string
  43. // The URL which should be added to alertmanagers config - Populated by Go-NEB after Service registration.
  44. WebhookURL string `json:"webhook_url"`
  45. // A map of matrix rooms to templates
  46. Rooms map[string]struct {
  47. TextTemplate string `json:"text_template"`
  48. HTMLTemplate string `json:"html_template"`
  49. MsgType string `json:"msg_type"`
  50. } `json:"rooms"`
  51. }
  52. // WebhookNotification is the payload from Alertmanager
  53. type WebhookNotification struct {
  54. Version string `json:"version"`
  55. GroupKey string `json:"groupKey"`
  56. Status string `json:"status"`
  57. Receiver string `json:"receiver"`
  58. GroupLabels map[string]string `json:"groupLabels"`
  59. CommonLabels map[string]string `json:"commonLabels"`
  60. CommonAnnotations map[string]string `json:"commonAnnotations"`
  61. ExternalURL string `json:"externalURL"`
  62. Alerts []struct {
  63. Status string `json:"status"`
  64. Labels map[string]string `json:"labels"`
  65. Annotations map[string]string `json:"annotations"`
  66. StartsAt string `json:"startsAt"`
  67. EndsAt string `json:"endsAt"`
  68. GeneratorURL string `json:"generatorURL"`
  69. SilenceURL string
  70. } `json:"alerts"`
  71. }
  72. // OnReceiveWebhook receives requests from Alertmanager and sends requests to Matrix as a result.
  73. func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *gomatrix.Client) {
  74. decoder := json.NewDecoder(req.Body)
  75. var notif WebhookNotification
  76. if err := decoder.Decode(&notif); err != nil {
  77. log.WithError(err).Error("Alertmanager webhook received an invalid JSON payload")
  78. w.WriteHeader(400)
  79. return
  80. }
  81. // add the silence link for each alert
  82. // see 'newSilenceFromAlertLabels' in
  83. // https://github.com/prometheus/alertmanager/blob/master/ui/app/src/Views/SilenceForm/Parsing.elm
  84. for i := range notif.Alerts {
  85. alert := &notif.Alerts[i]
  86. filters := []string{}
  87. for label, val := range alert.Labels {
  88. filters = append(filters, fmt.Sprintf("%s%%3D\"%s\"", label, val))
  89. }
  90. alert.SilenceURL = fmt.Sprintf("%s#silences/new?filter={%s}", notif.ExternalURL, strings.Join(filters, ","))
  91. }
  92. for roomID, templates := range s.Rooms {
  93. var msg interface{}
  94. // we don't check whether the templates parse because we already did when storing them in the db
  95. textTemplate, _ := text.New("textTemplate").Parse(templates.TextTemplate)
  96. var bodyBuffer bytes.Buffer
  97. if err := textTemplate.Execute(&bodyBuffer, notif); err != nil {
  98. log.WithError(err).Error("Alertmanager webhook failed to execute text template")
  99. w.WriteHeader(500)
  100. return
  101. }
  102. if templates.HTMLTemplate != "" {
  103. // we don't check whether the templates parse because we already did when storing them in the db
  104. htmlTemplate, _ := html.New("htmlTemplate").Parse(templates.HTMLTemplate)
  105. var formattedBodyBuffer bytes.Buffer
  106. if err := htmlTemplate.Execute(&formattedBodyBuffer, notif); err != nil {
  107. log.WithError(err).Error("Alertmanager webhook failed to execute HTML template")
  108. w.WriteHeader(500)
  109. return
  110. }
  111. msg = gomatrix.HTMLMessage{
  112. Body: bodyBuffer.String(),
  113. MsgType: templates.MsgType,
  114. Format: "org.matrix.custom.html",
  115. FormattedBody: formattedBodyBuffer.String(),
  116. }
  117. } else {
  118. msg = gomatrix.TextMessage{
  119. Body: bodyBuffer.String(),
  120. MsgType: templates.MsgType,
  121. }
  122. }
  123. log.WithFields(log.Fields{
  124. "message": msg,
  125. "room_id": roomID,
  126. }).Print("Sending Alertmanager notification to room")
  127. if _, e := cli.SendMessageEvent(roomID, "m.room.message", msg); e != nil {
  128. log.WithError(e).WithField("room_id", roomID).Print(
  129. "Failed to send Alertmanager notification to room.")
  130. }
  131. }
  132. w.WriteHeader(200)
  133. }
  134. // Register makes sure the Config information supplied is valid.
  135. func (s *Service) Register(oldService types.Service, client *gomatrix.Client) error {
  136. s.WebhookURL = s.webhookEndpointURL
  137. for _, templates := range s.Rooms {
  138. // validate that we have at least a plain text template
  139. if templates.TextTemplate == "" {
  140. return fmt.Errorf("plain text template missing")
  141. }
  142. // validate the plain text template is valid
  143. _, err := text.New("textTemplate").Parse(templates.TextTemplate)
  144. if err != nil {
  145. return fmt.Errorf("plain text template is invalid: %v", err)
  146. }
  147. if templates.HTMLTemplate != "" {
  148. // validate that the html template is valid
  149. _, err := html.New("htmlTemplate").Parse(templates.HTMLTemplate)
  150. if err != nil {
  151. return fmt.Errorf("html template is invalid: %v", err)
  152. }
  153. }
  154. // validate that the msgtype is either m.notice or m.text
  155. if templates.MsgType != "m.notice" && templates.MsgType != "m.text" {
  156. return fmt.Errorf("msg_type is neither 'm.notice' nor 'm.text'")
  157. }
  158. }
  159. s.joinRooms(client)
  160. return nil
  161. }
  162. // PostRegister deletes this service if there are no registered repos.
  163. func (s *Service) PostRegister(oldService types.Service) {
  164. // At least one room still active
  165. if len(s.Rooms) > 0 {
  166. return
  167. }
  168. // Delete this service since no repos are configured
  169. logger := log.WithFields(log.Fields{
  170. "service_type": s.ServiceType(),
  171. "service_id": s.ServiceID(),
  172. })
  173. logger.Info("Removing service as no repositories are registered.")
  174. if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil {
  175. logger.WithError(err).Error("Failed to delete service")
  176. }
  177. }
  178. func (s *Service) joinRooms(client *gomatrix.Client) {
  179. for roomID := range s.Rooms {
  180. if _, err := client.JoinRoom(roomID, "", nil); err != nil {
  181. log.WithFields(log.Fields{
  182. log.ErrorKey: err,
  183. "room_id": roomID,
  184. "user_id": client.UserID,
  185. }).Error("Failed to join room")
  186. }
  187. }
  188. }
  189. func init() {
  190. types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service {
  191. return &Service{
  192. DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType),
  193. webhookEndpointURL: webhookEndpointURL,
  194. }
  195. })
  196. }