pushover.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. package notifiers
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "mime/multipart"
  7. "os"
  8. "strconv"
  9. "github.com/grafana/grafana/pkg/bus"
  10. "github.com/grafana/grafana/pkg/infra/log"
  11. "github.com/grafana/grafana/pkg/models"
  12. "github.com/grafana/grafana/pkg/services/alerting"
  13. )
  14. const pushoverEndpoint = "https://api.pushover.net/1/messages.json"
  15. func init() {
  16. sounds := `
  17. 'default',
  18. 'pushover',
  19. 'bike',
  20. 'bugle',
  21. 'cashregister',
  22. 'classical',
  23. 'cosmic',
  24. 'falling',
  25. 'gamelan',
  26. 'incoming',
  27. 'intermission',
  28. 'magic',
  29. 'mechanical',
  30. 'pianobar',
  31. 'siren',
  32. 'spacealarm',
  33. 'tugboat',
  34. 'alien',
  35. 'climb',
  36. 'persistent',
  37. 'echo',
  38. 'updown',
  39. 'none'`
  40. alerting.RegisterNotifier(&alerting.NotifierPlugin{
  41. Type: "pushover",
  42. Name: "Pushover",
  43. Description: "Sends HTTP POST request to the Pushover API",
  44. Factory: NewPushoverNotifier,
  45. OptionsTemplate: `
  46. <h3 class="page-heading">Pushover settings</h3>
  47. <div class="gf-form">
  48. <span class="gf-form-label width-10">API Token</span>
  49. <input type="text" class="gf-form-input" required placeholder="Application token" ng-model="ctrl.model.settings.apiToken"></input>
  50. </div>
  51. <div class="gf-form">
  52. <span class="gf-form-label width-10">User key(s)</span>
  53. <input type="text" class="gf-form-input" required placeholder="comma-separated list" ng-model="ctrl.model.settings.userKey"></input>
  54. </div>
  55. <div class="gf-form">
  56. <span class="gf-form-label width-10">Device(s) (optional)</span>
  57. <input type="text" class="gf-form-input" placeholder="comma-separated list; leave empty to send to all devices" ng-model="ctrl.model.settings.device"></input>
  58. </div>
  59. <div class="gf-form">
  60. <span class="gf-form-label width-10">Priority</span>
  61. <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.priority" ng-options="v as k for (k, v) in {
  62. Emergency: '2',
  63. High: '1',
  64. Normal: '0',
  65. Low: '-1',
  66. Lowest: '-2'
  67. }" ng-init="ctrl.model.settings.priority=ctrl.model.settings.priority||'0'"></select>
  68. </div>
  69. <div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
  70. <span class="gf-form-label width-10">Retry</span>
  71. <input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="minimum 30 seconds" ng-model="ctrl.model.settings.retry" ng-init="ctrl.model.settings.retry=ctrl.model.settings.retry||'60'></input>
  72. </div>
  73. <div class="gf-form" ng-show="ctrl.model.settings.priority == '2'">
  74. <span class="gf-form-label width-10">Expire</span>
  75. <input type="text" class="gf-form-input max-width-14" ng-required="ctrl.model.settings.priority == '2'" placeholder="maximum 86400 seconds" ng-model="ctrl.model.settings.expire" ng-init="ctrl.model.settings.expire=ctrl.model.settings.expire||'3600'"></input>
  76. </div>
  77. <div class="gf-form">
  78. <span class="gf-form-label width-10">Alerting sound</span>
  79. <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.sound" ng-options="s for s in [
  80. ` + sounds + `
  81. ]" ng-init="ctrl.model.settings.sound=ctrl.model.settings.sound||'default'"></select>
  82. </div>
  83. <div class="gf-form">
  84. <span class="gf-form-label width-10">OK sound</span>
  85. <select class="gf-form-input max-width-14" ng-model="ctrl.model.settings.okSound" ng-options="s for s in [
  86. ` + sounds + `
  87. ]" ng-init="ctrl.model.settings.okSound=ctrl.model.settings.okSound||'default'"></select>
  88. </div>
  89. `,
  90. })
  91. }
  92. // NewPushoverNotifier is the constructor for the Pushover Notifier
  93. func NewPushoverNotifier(model *models.AlertNotification) (alerting.Notifier, error) {
  94. userKey := model.Settings.Get("userKey").MustString()
  95. APIToken := model.Settings.Get("apiToken").MustString()
  96. device := model.Settings.Get("device").MustString()
  97. priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
  98. retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
  99. expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
  100. alertingSound := model.Settings.Get("sound").MustString()
  101. okSound := model.Settings.Get("okSound").MustString()
  102. uploadImage := model.Settings.Get("uploadImage").MustBool(true)
  103. if userKey == "" {
  104. return nil, alerting.ValidationError{Reason: "User key not given"}
  105. }
  106. if APIToken == "" {
  107. return nil, alerting.ValidationError{Reason: "API token not given"}
  108. }
  109. return &PushoverNotifier{
  110. NotifierBase: NewNotifierBase(model),
  111. UserKey: userKey,
  112. APIToken: APIToken,
  113. Priority: priority,
  114. Retry: retry,
  115. Expire: expire,
  116. Device: device,
  117. AlertingSound: alertingSound,
  118. OkSound: okSound,
  119. Upload: uploadImage,
  120. log: log.New("alerting.notifier.pushover"),
  121. }, nil
  122. }
  123. // PushoverNotifier is responsible for sending
  124. // alert notifications to Pushover
  125. type PushoverNotifier struct {
  126. NotifierBase
  127. UserKey string
  128. APIToken string
  129. Priority int
  130. Retry int
  131. Expire int
  132. Device string
  133. AlertingSound string
  134. OkSound string
  135. Upload bool
  136. log log.Logger
  137. }
  138. // Notify sends a alert notification to Pushover
  139. func (pn *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
  140. ruleURL, err := evalContext.GetRuleURL()
  141. if err != nil {
  142. pn.log.Error("Failed get rule link", "error", err)
  143. return err
  144. }
  145. message := evalContext.Rule.Message
  146. for idx, evt := range evalContext.EvalMatches {
  147. message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
  148. if idx > 4 {
  149. break
  150. }
  151. }
  152. if evalContext.Error != nil {
  153. message += fmt.Sprintf("\n<b>Error message:</b> %s", evalContext.Error.Error())
  154. }
  155. if message == "" {
  156. message = "Notification message missing (Set a notification message to replace this text.)"
  157. }
  158. headers, uploadBody, err := pn.genPushoverBody(evalContext, message, ruleURL)
  159. if err != nil {
  160. pn.log.Error("Failed to generate body for pushover", "error", err)
  161. return err
  162. }
  163. cmd := &models.SendWebhookSync{
  164. Url: pushoverEndpoint,
  165. HttpMethod: "POST",
  166. HttpHeader: headers,
  167. Body: uploadBody.String(),
  168. }
  169. if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
  170. pn.log.Error("Failed to send pushover notification", "error", err, "webhook", pn.Name)
  171. return err
  172. }
  173. return nil
  174. }
  175. func (pn *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleURL string) (map[string]string, bytes.Buffer, error) {
  176. var b bytes.Buffer
  177. var err error
  178. w := multipart.NewWriter(&b)
  179. // Add image only if requested and available
  180. if pn.Upload && evalContext.ImageOnDiskPath != "" {
  181. f, err := os.Open(evalContext.ImageOnDiskPath)
  182. if err != nil {
  183. return nil, b, err
  184. }
  185. defer f.Close()
  186. fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
  187. if err != nil {
  188. return nil, b, err
  189. }
  190. _, err = io.Copy(fw, f)
  191. if err != nil {
  192. return nil, b, err
  193. }
  194. }
  195. // Add the user token
  196. err = w.WriteField("user", pn.UserKey)
  197. if err != nil {
  198. return nil, b, err
  199. }
  200. // Add the api token
  201. err = w.WriteField("token", pn.APIToken)
  202. if err != nil {
  203. return nil, b, err
  204. }
  205. // Add priority
  206. err = w.WriteField("priority", strconv.Itoa(pn.Priority))
  207. if err != nil {
  208. return nil, b, err
  209. }
  210. if pn.Priority == 2 {
  211. err = w.WriteField("retry", strconv.Itoa(pn.Retry))
  212. if err != nil {
  213. return nil, b, err
  214. }
  215. err = w.WriteField("expire", strconv.Itoa(pn.Expire))
  216. if err != nil {
  217. return nil, b, err
  218. }
  219. }
  220. // Add device
  221. if pn.Device != "" {
  222. err = w.WriteField("device", pn.Device)
  223. if err != nil {
  224. return nil, b, err
  225. }
  226. }
  227. // Add sound
  228. sound := pn.AlertingSound
  229. if evalContext.Rule.State == models.AlertStateOK {
  230. sound = pn.OkSound
  231. }
  232. if sound != "default" {
  233. err = w.WriteField("sound", sound)
  234. if err != nil {
  235. return nil, b, err
  236. }
  237. }
  238. // Add title
  239. err = w.WriteField("title", evalContext.GetNotificationTitle())
  240. if err != nil {
  241. return nil, b, err
  242. }
  243. // Add URL
  244. err = w.WriteField("url", ruleURL)
  245. if err != nil {
  246. return nil, b, err
  247. }
  248. // Add URL title
  249. err = w.WriteField("url_title", "Show dashboard with alert")
  250. if err != nil {
  251. return nil, b, err
  252. }
  253. // Add message
  254. err = w.WriteField("message", message)
  255. if err != nil {
  256. return nil, b, err
  257. }
  258. // Mark as html message
  259. err = w.WriteField("html", "1")
  260. if err != nil {
  261. return nil, b, err
  262. }
  263. w.Close()
  264. headers := map[string]string{
  265. "Content-Type": w.FormDataContentType(),
  266. }
  267. return headers, b, nil
  268. }