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.

279 lines
8.0 KiB

  1. from django.conf import settings
  2. from django.template.loader import render_to_string
  3. from django.utils import timezone
  4. import json
  5. import requests
  6. from six.moves.urllib.parse import quote
  7. from hc.lib import emails
  8. def tmpl(template_name, **ctx):
  9. template_path = "integrations/%s" % template_name
  10. return render_to_string(template_path, ctx).strip()
  11. class Transport(object):
  12. def __init__(self, channel):
  13. self.channel = channel
  14. def notify(self, check):
  15. """ Send notification about current status of the check.
  16. This method returns None on success, and error message
  17. on error.
  18. """
  19. raise NotImplementedError()
  20. def is_noop(self, check):
  21. """ Return True if transport will ignore check's current status.
  22. This method is overriden in Webhook subclass where the user can
  23. configure webhook urls for "up" and "down" events, and both are
  24. optional.
  25. """
  26. return False
  27. def checks(self):
  28. return self.channel.user.check_set.order_by("created")
  29. class Email(Transport):
  30. def notify(self, check, bounce_url):
  31. if not self.channel.email_verified:
  32. return "Email not verified"
  33. headers = {"X-Bounce-Url": bounce_url}
  34. ctx = {
  35. "check": check,
  36. "checks": self.checks(),
  37. "now": timezone.now(),
  38. "unsub_link": self.channel.get_unsub_link()
  39. }
  40. emails.alert(self.channel.value, ctx, headers)
  41. class HttpTransport(Transport):
  42. def _request(self, method, url, **kwargs):
  43. try:
  44. options = dict(kwargs)
  45. if "headers" not in options:
  46. options["headers"] = {}
  47. options["timeout"] = 5
  48. options["headers"]["User-Agent"] = "healthchecks.io"
  49. r = requests.request(method, url, **options)
  50. if r.status_code not in (200, 201, 204):
  51. return "Received status code %d" % r.status_code
  52. except requests.exceptions.Timeout:
  53. # Well, we tried
  54. return "Connection timed out"
  55. except requests.exceptions.ConnectionError:
  56. return "Connection failed"
  57. def get(self, url):
  58. # Make 3 attempts--
  59. for x in range(0, 3):
  60. error = self._request("get", url)
  61. if error is None:
  62. break
  63. return error
  64. def post(self, url, **kwargs):
  65. # Make 3 attempts--
  66. for x in range(0, 3):
  67. error = self._request("post", url, **kwargs)
  68. if error is None:
  69. break
  70. return error
  71. class Webhook(HttpTransport):
  72. def prepare(self, template, check, urlencode=False):
  73. """ Replace variables with actual values.
  74. There should be no bad translations if users use $ symbol in
  75. check's name or tags, because $ gets urlencoded to %24
  76. """
  77. def safe(s):
  78. return quote(s) if urlencode else s
  79. result = template
  80. if "$CODE" in result:
  81. result = result.replace("$CODE", str(check.code))
  82. if "$STATUS" in result:
  83. result = result.replace("$STATUS", check.status)
  84. if "$NOW" in result:
  85. s = timezone.now().replace(microsecond=0).isoformat()
  86. result = result.replace("$NOW", safe(s))
  87. if "$NAME" in result:
  88. result = result.replace("$NAME", safe(check.name))
  89. if "$TAG" in result:
  90. for i, tag in enumerate(check.tags_list()):
  91. placeholder = "$TAG%d" % (i + 1)
  92. result = result.replace(placeholder, safe(tag))
  93. return result
  94. def is_noop(self, check):
  95. if check.status == "down" and not self.channel.value_down:
  96. return True
  97. if check.status == "up" and not self.channel.value_up:
  98. return True
  99. return False
  100. def notify(self, check):
  101. url = self.channel.value_down
  102. if check.status == "up":
  103. url = self.channel.value_up
  104. assert url
  105. url = self.prepare(url, check, urlencode=True)
  106. if self.channel.post_data:
  107. payload = self.prepare(self.channel.post_data, check)
  108. return self.post(url, data=payload)
  109. else:
  110. return self.get(url)
  111. class Slack(HttpTransport):
  112. def notify(self, check):
  113. text = tmpl("slack_message.json", check=check)
  114. payload = json.loads(text)
  115. return self.post(self.channel.slack_webhook_url, json=payload)
  116. class HipChat(HttpTransport):
  117. def notify(self, check):
  118. text = tmpl("hipchat_message.html", check=check)
  119. payload = {
  120. "message": text,
  121. "color": "green" if check.status == "up" else "red",
  122. }
  123. return self.post(self.channel.value, json=payload)
  124. class OpsGenie(HttpTransport):
  125. def notify(self, check):
  126. payload = {
  127. "apiKey": self.channel.value,
  128. "alias": str(check.code),
  129. "source": "healthchecks.io"
  130. }
  131. if check.status == "down":
  132. payload["tags"] = ",".join(check.tags_list())
  133. payload["message"] = tmpl("opsgenie_message.html", check=check)
  134. payload["note"] = tmpl("opsgenie_note.html", check=check)
  135. url = "https://api.opsgenie.com/v1/json/alert"
  136. if check.status == "up":
  137. url += "/close"
  138. return self.post(url, json=payload)
  139. class PagerDuty(HttpTransport):
  140. URL = "https://events.pagerduty.com/generic/2010-04-15/create_event.json"
  141. def notify(self, check):
  142. description = tmpl("pd_description.html", check=check)
  143. payload = {
  144. "service_key": self.channel.value,
  145. "incident_key": str(check.code),
  146. "event_type": "trigger" if check.status == "down" else "resolve",
  147. "description": description,
  148. "client": "healthchecks.io",
  149. "client_url": settings.SITE_ROOT
  150. }
  151. return self.post(self.URL, json=payload)
  152. class Pushbullet(HttpTransport):
  153. def notify(self, check):
  154. text = tmpl("pushbullet_message.html", check=check)
  155. url = "https://api.pushbullet.com/v2/pushes"
  156. headers = {
  157. "Access-Token": self.channel.value,
  158. "Conent-Type": "application/json"
  159. }
  160. payload = {
  161. "type": "note",
  162. "title": "healthchecks.io",
  163. "body": text
  164. }
  165. return self.post(url, json=payload, headers=headers)
  166. class Pushover(HttpTransport):
  167. URL = "https://api.pushover.net/1/messages.json"
  168. def notify(self, check):
  169. others = self.checks().filter(status="down").exclude(code=check.code)
  170. ctx = {
  171. "check": check,
  172. "down_checks": others,
  173. }
  174. text = tmpl("pushover_message.html", **ctx)
  175. title = tmpl("pushover_title.html", **ctx)
  176. user_key, prio = self.channel.value.split("|")
  177. payload = {
  178. "token": settings.PUSHOVER_API_TOKEN,
  179. "user": user_key,
  180. "message": text,
  181. "title": title,
  182. "html": 1,
  183. "priority": int(prio),
  184. }
  185. # Emergency notification
  186. if prio == "2":
  187. payload["retry"] = settings.PUSHOVER_EMERGENCY_RETRY_DELAY
  188. payload["expire"] = settings.PUSHOVER_EMERGENCY_EXPIRATION
  189. return self.post(self.URL, data=payload)
  190. class VictorOps(HttpTransport):
  191. def notify(self, check):
  192. description = tmpl("victorops_description.html", check=check)
  193. mtype = "CRITICAL" if check.status == "down" else "RECOVERY"
  194. payload = {
  195. "entity_id": str(check.code),
  196. "message_type": mtype,
  197. "entity_display_name": check.name_then_code(),
  198. "state_message": description,
  199. "monitoring_tool": "healthchecks.io",
  200. }
  201. return self.post(self.channel.value, json=payload)
  202. class Discord(HttpTransport):
  203. def notify(self, check):
  204. text = tmpl("slack_message.json", check=check)
  205. payload = json.loads(text)
  206. url = self.channel.discord_webhook_url + "/slack"
  207. return self.post(url, json=payload)