utils.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import asyncio
  2. import logging
  3. import time
  4. from datetime import timedelta
  5. from ssl import SSLError
  6. import aiohttp
  7. from aiohttp import ClientTimeout
  8. from aiohttp.client_exceptions import ClientConnectorError
  9. from django.conf import settings
  10. from django.utils import timezone
  11. from .constants import MonitorCheckReason, MonitorType
  12. from .models import MonitorCheck
  13. logger = logging.getLogger(__name__)
  14. DEFAULT_TIMEOUT = 20 # Seconds
  15. PAYLOAD_LIMIT = 2_000_000 # 2mb
  16. PAYLOAD_SAVE_LIMIT = 500_000 # pseudo 500kb
  17. async def process_response(monitor, response):
  18. if response.status == monitor["expected_status"]:
  19. if monitor["expected_body"]:
  20. # Limit size to 2MB
  21. body = await response.content.read(PAYLOAD_LIMIT)
  22. try:
  23. encoding = response.get_encoding()
  24. payload = body.decode(encoding, errors="ignore")
  25. except RuntimeError:
  26. payload = body.decode(errors="ignore")
  27. if monitor["expected_body"] in payload:
  28. monitor["is_up"] = True
  29. else:
  30. monitor["reason"] = MonitorCheckReason.BODY
  31. if monitor["latest_is_up"] != monitor["is_up"]:
  32. # Save only first 500k chars, to roughly reduce disk usage
  33. # Note that a unicode char is not always one byte
  34. # Only save on changes
  35. monitor["data"] = {"payload": payload[:PAYLOAD_SAVE_LIMIT]}
  36. else:
  37. monitor["is_up"] = True
  38. else:
  39. monitor["reason"] = MonitorCheckReason.STATUS
  40. async def fetch(session, monitor):
  41. monitor["is_up"] = False
  42. if monitor["monitor_type"] == MonitorType.HEARTBEAT:
  43. interval = timedelta(seconds=monitor["interval"])
  44. if await MonitorCheck.objects.filter(
  45. monitor_id=monitor["id"],
  46. start_check__gte=timezone.now() - interval,
  47. ).aexists():
  48. monitor["is_up"] = True
  49. return monitor
  50. url = monitor["url"]
  51. timeout = monitor["timeout"] or DEFAULT_TIMEOUT
  52. try:
  53. start = time.monotonic()
  54. if monitor["monitor_type"] == MonitorType.PORT:
  55. fut = asyncio.open_connection(*url.split(":"))
  56. await asyncio.wait_for(fut, timeout=timeout)
  57. monitor["is_up"] = True
  58. else:
  59. client_timeout = ClientTimeout(total=monitor["timeout"] or DEFAULT_TIMEOUT)
  60. if monitor["monitor_type"] == MonitorType.PING:
  61. async with session.head(url, timeout=client_timeout):
  62. monitor["is_up"] = True
  63. elif monitor["monitor_type"] == MonitorType.GET:
  64. async with session.get(url, timeout=client_timeout) as response:
  65. await process_response(monitor, response)
  66. elif monitor["monitor_type"] == MonitorType.POST:
  67. async with session.post(url, timeout=client_timeout) as response:
  68. await process_response(monitor, response)
  69. monitor["response_time"] = (
  70. timedelta(seconds=time.monotonic() - start).total_seconds() * 1000
  71. )
  72. except SSLError:
  73. monitor["reason"] = MonitorCheckReason.SSL
  74. except asyncio.TimeoutError:
  75. monitor["reason"] = MonitorCheckReason.TIMEOUT
  76. except ClientConnectorError:
  77. monitor["reason"] = MonitorCheckReason.NETWORK
  78. except OSError:
  79. monitor["reason"] = MonitorCheckReason.UNKNOWN
  80. except Exception as e:
  81. logger.error(f"Monitor {monitor['id']} check failed", exc_info=e)
  82. monitor["reason"] = MonitorCheckReason.UNKNOWN
  83. return monitor
  84. async def fetch_all(monitors):
  85. async with aiohttp.ClientSession(
  86. headers={"User-Agent": "GlitchTip/" + settings.GLITCHTIP_VERSION}
  87. ) as session:
  88. results = await asyncio.gather(
  89. *[fetch(session, monitor) for monitor in monitors], return_exceptions=True
  90. )
  91. return results