pushover.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  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. m "github.com/grafana/grafana/pkg/models"
  12. "github.com/grafana/grafana/pkg/services/alerting"
  13. )
  14. const PUSHOVER_ENDPOINT = "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. func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
  93. userKey := model.Settings.Get("userKey").MustString()
  94. apiToken := model.Settings.Get("apiToken").MustString()
  95. device := model.Settings.Get("device").MustString()
  96. priority, _ := strconv.Atoi(model.Settings.Get("priority").MustString())
  97. retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString())
  98. expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString())
  99. alertingSound := model.Settings.Get("sound").MustString()
  100. okSound := model.Settings.Get("okSound").MustString()
  101. uploadImage := model.Settings.Get("uploadImage").MustBool(true)
  102. if userKey == "" {
  103. return nil, alerting.ValidationError{Reason: "User key not given"}
  104. }
  105. if apiToken == "" {
  106. return nil, alerting.ValidationError{Reason: "API token not given"}
  107. }
  108. return &PushoverNotifier{
  109. NotifierBase: NewNotifierBase(model),
  110. UserKey: userKey,
  111. ApiToken: apiToken,
  112. Priority: priority,
  113. Retry: retry,
  114. Expire: expire,
  115. Device: device,
  116. AlertingSound: alertingSound,
  117. OkSound: okSound,
  118. Upload: uploadImage,
  119. log: log.New("alerting.notifier.pushover"),
  120. }, nil
  121. }
  122. type PushoverNotifier struct {
  123. NotifierBase
  124. UserKey string
  125. ApiToken string
  126. Priority int
  127. Retry int
  128. Expire int
  129. Device string
  130. AlertingSound string
  131. OkSound string
  132. Upload bool
  133. log log.Logger
  134. }
  135. func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
  136. ruleUrl, err := evalContext.GetRuleUrl()
  137. if err != nil {
  138. this.log.Error("Failed get rule link", "error", err)
  139. return err
  140. }
  141. message := evalContext.Rule.Message
  142. for idx, evt := range evalContext.EvalMatches {
  143. message += fmt.Sprintf("\n<b>%s</b>: %v", evt.Metric, evt.Value)
  144. if idx > 4 {
  145. break
  146. }
  147. }
  148. if evalContext.Error != nil {
  149. message += fmt.Sprintf("\n<b>Error message:</b> %s", evalContext.Error.Error())
  150. }
  151. if message == "" {
  152. message = "Notification message missing (Set a notification message to replace this text.)"
  153. }
  154. headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
  155. if err != nil {
  156. this.log.Error("Failed to generate body for pushover", "error", err)
  157. return err
  158. }
  159. cmd := &m.SendWebhookSync{
  160. Url: PUSHOVER_ENDPOINT,
  161. HttpMethod: "POST",
  162. HttpHeader: headers,
  163. Body: uploadBody.String(),
  164. }
  165. if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil {
  166. this.log.Error("Failed to send pushover notification", "error", err, "webhook", this.Name)
  167. return err
  168. }
  169. return nil
  170. }
  171. func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) {
  172. var b bytes.Buffer
  173. var err error
  174. w := multipart.NewWriter(&b)
  175. // Add image only if requested and available
  176. if this.Upload && evalContext.ImageOnDiskPath != "" {
  177. f, err := os.Open(evalContext.ImageOnDiskPath)
  178. if err != nil {
  179. return nil, b, err
  180. }
  181. defer f.Close()
  182. fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath)
  183. if err != nil {
  184. return nil, b, err
  185. }
  186. _, err = io.Copy(fw, f)
  187. if err != nil {
  188. return nil, b, err
  189. }
  190. }
  191. // Add the user token
  192. err = w.WriteField("user", this.UserKey)
  193. if err != nil {
  194. return nil, b, err
  195. }
  196. // Add the api token
  197. err = w.WriteField("token", this.ApiToken)
  198. if err != nil {
  199. return nil, b, err
  200. }
  201. // Add priority
  202. err = w.WriteField("priority", strconv.Itoa(this.Priority))
  203. if err != nil {
  204. return nil, b, err
  205. }
  206. if this.Priority == 2 {
  207. err = w.WriteField("retry", strconv.Itoa(this.Retry))
  208. if err != nil {
  209. return nil, b, err
  210. }
  211. err = w.WriteField("expire", strconv.Itoa(this.Expire))
  212. if err != nil {
  213. return nil, b, err
  214. }
  215. }
  216. // Add device
  217. if this.Device != "" {
  218. err = w.WriteField("device", this.Device)
  219. if err != nil {
  220. return nil, b, err
  221. }
  222. }
  223. // Add sound
  224. sound := this.AlertingSound
  225. if evalContext.Rule.State == m.AlertStateOK {
  226. sound = this.OkSound
  227. }
  228. if sound != "default" {
  229. err = w.WriteField("sound", sound)
  230. if err != nil {
  231. return nil, b, err
  232. }
  233. }
  234. // Add title
  235. err = w.WriteField("title", evalContext.GetNotificationTitle())
  236. if err != nil {
  237. return nil, b, err
  238. }
  239. // Add URL
  240. err = w.WriteField("url", ruleUrl)
  241. if err != nil {
  242. return nil, b, err
  243. }
  244. // Add URL title
  245. err = w.WriteField("url_title", "Show dashboard with alert")
  246. if err != nil {
  247. return nil, b, err
  248. }
  249. // Add message
  250. err = w.WriteField("message", message)
  251. if err != nil {
  252. return nil, b, err
  253. }
  254. // Mark as html message
  255. err = w.WriteField("html", "1")
  256. if err != nil {
  257. return nil, b, err
  258. }
  259. w.Close()
  260. headers := map[string]string{
  261. "Content-Type": w.FormDataContentType(),
  262. }
  263. return headers, b, nil
  264. }