pushover.go 7.8 KB

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