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.

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