healthchecks.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. import logging
  2. import re
  3. import requests
  4. import borgmatic.hooks.monitoring.logs
  5. from borgmatic.hooks.monitoring import monitor
  6. logger = logging.getLogger(__name__)
  7. MONITOR_STATE_TO_HEALTHCHECKS = {
  8. monitor.State.START: 'start',
  9. monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state.
  10. monitor.State.FAIL: 'fail',
  11. monitor.State.LOG: 'log',
  12. }
  13. DEFAULT_PING_BODY_LIMIT_BYTES = 100000
  14. HANDLER_IDENTIFIER = 'healthchecks'
  15. TIMEOUT_SECONDS = 10
  16. def initialize_monitor(hook_config, config, config_filename, monitoring_log_level, dry_run):
  17. '''
  18. Add a handler to the root logger that stores in memory the most recent logs emitted. That way,
  19. we can send them all to Healthchecks upon a finish or failure state. But skip this if the
  20. "send_logs" option is false.
  21. '''
  22. if hook_config.get('send_logs') is False:
  23. return
  24. ping_body_limit = max(
  25. hook_config.get('ping_body_limit', DEFAULT_PING_BODY_LIMIT_BYTES)
  26. - len(borgmatic.hooks.monitoring.logs.PAYLOAD_TRUNCATION_INDICATOR),
  27. 0,
  28. )
  29. borgmatic.hooks.monitoring.logs.add_handler(
  30. borgmatic.hooks.monitoring.logs.Forgetful_buffering_handler(
  31. HANDLER_IDENTIFIER,
  32. ping_body_limit,
  33. monitoring_log_level,
  34. ),
  35. )
  36. def ping_monitor(hook_config, config, config_filename, state, monitoring_log_level, dry_run):
  37. '''
  38. Ping the configured Healthchecks URL or UUID, modified with the monitor.State. Use the given
  39. configuration filename in any log entries, and log to Healthchecks with the giving log level.
  40. If this is a dry run, then don't actually ping anything.
  41. '''
  42. ping_url = (
  43. hook_config['ping_url']
  44. if hook_config['ping_url'].startswith('http')
  45. else f"https://hc-ping.com/{hook_config['ping_url']}"
  46. )
  47. dry_run_label = ' (dry run; not actually pinging)' if dry_run else ''
  48. if 'states' in hook_config and state.name.lower() not in hook_config['states']:
  49. logger.info(f'Skipping Healthchecks {state.name.lower()} ping due to configured states')
  50. return
  51. ping_url_is_uuid = re.search(r'\w{8}-\w{4}-\w{4}-\w{4}-\w{12}$', ping_url)
  52. healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state)
  53. if healthchecks_state:
  54. ping_url = f'{ping_url}/{healthchecks_state}'
  55. if hook_config.get('create_slug'):
  56. if ping_url_is_uuid:
  57. logger.warning('Healthchecks UUIDs do not support auto provisionning; ignoring')
  58. else:
  59. ping_url = f'{ping_url}?create=1'
  60. logger.info(f'Pinging Healthchecks {state.name.lower()}{dry_run_label}')
  61. logger.debug(f'Using Healthchecks ping URL {ping_url}')
  62. if state in {monitor.State.FINISH, monitor.State.FAIL, monitor.State.LOG}:
  63. payload = borgmatic.hooks.monitoring.logs.format_buffered_logs_for_payload(
  64. HANDLER_IDENTIFIER,
  65. )
  66. else:
  67. payload = ''
  68. if not dry_run:
  69. logging.getLogger('urllib3').setLevel(logging.ERROR)
  70. try:
  71. response = requests.post(
  72. ping_url,
  73. data=payload.encode('utf-8'),
  74. verify=hook_config.get('verify_tls', True),
  75. timeout=TIMEOUT_SECONDS,
  76. headers={'User-Agent': 'borgmatic'},
  77. )
  78. if not response.ok:
  79. response.raise_for_status()
  80. except requests.exceptions.RequestException as error:
  81. logger.warning(f'Healthchecks error: {error}')
  82. def destroy_monitor(hook_config, config, monitoring_log_level, dry_run):
  83. '''
  84. Remove the monitor handler that was added to the root logger. This prevents the handler from
  85. getting reused by other instances of this monitor.
  86. '''
  87. borgmatic.hooks.monitoring.logs.remove_handler(HANDLER_IDENTIFIER)