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.

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