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.

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