healthchecks.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. import logging
  2. import requests
  3. from borgmatic.hooks import monitor
  4. logger = logging.getLogger(__name__)
  5. MONITOR_STATE_TO_HEALTHCHECKS = {
  6. monitor.State.START: 'start',
  7. monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
  8. monitor.State.FAIL: 'fail',
  9. }
  10. PAYLOAD_TRUNCATION_INDICATOR = '...\n'
  11. PAYLOAD_LIMIT_BYTES = 10 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR)
  12. class Forgetful_buffering_handler(logging.Handler):
  13. '''
  14. A buffering log handler that stores log messages in memory, and throws away messages (oldest
  15. first) once a particular capacity in bytes is reached.
  16. '''
  17. def __init__(self, byte_capacity, log_level):
  18. super().__init__()
  19. self.byte_capacity = byte_capacity
  20. self.byte_count = 0
  21. self.buffer = []
  22. self.forgot = False
  23. self.setLevel(log_level)
  24. def emit(self, record):
  25. message = record.getMessage() + '\n'
  26. self.byte_count += len(message)
  27. self.buffer.append(message)
  28. while self.byte_count > self.byte_capacity and self.buffer:
  29. self.byte_count -= len(self.buffer[0])
  30. self.buffer.pop(0)
  31. self.forgot = True
  32. def format_buffered_logs_for_payload():
  33. '''
  34. Get the handler previously added to the root logger, and slurp buffered logs out of it to
  35. send to Healthchecks.
  36. '''
  37. try:
  38. buffering_handler = next(
  39. handler
  40. for handler in logging.getLogger().handlers
  41. if isinstance(handler, Forgetful_buffering_handler)
  42. )
  43. except StopIteration:
  44. # No handler means no payload.
  45. return ''
  46. payload = ''.join(message for message in buffering_handler.buffer)
  47. if buffering_handler.forgot:
  48. return PAYLOAD_TRUNCATION_INDICATOR + payload
  49. return payload
  50. def initialize_monitor(
  51. ping_url_or_uuid, config_filename, monitoring_log_level, dry_run
  52. ): # pragma: no cover
  53. '''
  54. Add a handler to the root logger that stores in memory the most recent logs emitted. That
  55. way, we can send them all to Healthchecks upon a finish or failure state.
  56. '''
  57. logging.getLogger().addHandler(
  58. Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES, monitoring_log_level)
  59. )
  60. def ping_monitor(ping_url_or_uuid, config_filename, state, monitoring_log_level, dry_run):
  61. '''
  62. Ping the given Healthchecks URL or UUID, modified with the monitor.State. Use the given
  63. configuration filename in any log entries, and log to Healthchecks with the giving log level.
  64. If this is a dry run, then don't actually ping anything.
  65. '''
  66. ping_url = (
  67. ping_url_or_uuid
  68. if ping_url_or_uuid.startswith('http')
  69. else 'https://hc-ping.com/{}'.format(ping_url_or_uuid)
  70. )
  71. dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
  72. healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
  73. if healthchecks_state:
  74. ping_url = '{}/{}'.format(ping_url, healthchecks_state)
  75. logger.info(
  76. '{}: Pinging Healthchecks {}{}'.format(config_filename, state.name.lower(), dry_run_label)
  77. )
  78. logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url))
  79. if state in (monitor.State.FINISH, monitor.State.FAIL):
  80. payload = format_buffered_logs_for_payload()
  81. else:
  82. payload = ''
  83. if not dry_run:
  84. logging.getLogger('urllib3').setLevel(logging.ERROR)
  85. requests.post(ping_url, data=payload.encode('utf-8'))
  86. def destroy_monitor(ping_url_or_uuid, config_filename, monitoring_log_level, dry_run):
  87. '''
  88. Remove the monitor handler that was added to the root logger. This prevents the handler from
  89. getting reused by other instances of this monitor.
  90. '''
  91. logger = logging.getLogger()
  92. for handler in tuple(logger.handlers):
  93. if isinstance(handler, Forgetful_buffering_handler):
  94. logger.removeHandler(handler)