import logging import requests from borgmatic.hooks import monitor logger = logging.getLogger(__name__) MONITOR_STATE_TO_HEALTHCHECKS = { monitor.State.START: 'start', monitor.State.FINISH: None, # Healthchecks doesn't append to the URL for the finished state. monitor.State.FAIL: 'fail', } PAYLOAD_TRUNCATION_INDICATOR = '...\n' PAYLOAD_LIMIT_BYTES = 10 * 1024 - len(PAYLOAD_TRUNCATION_INDICATOR) class Forgetful_buffering_handler(logging.Handler): ''' A buffering log handler that stores log messages in memory, and throws away messages (oldest first) once a particular capacity in bytes is reached. ''' def __init__(self, byte_capacity): super().__init__() self.byte_capacity = byte_capacity self.byte_count = 0 self.buffer = [] self.forgot = False def emit(self, record): message = record.getMessage() + '\n' self.byte_count += len(message) self.buffer.append(message) while self.byte_count > self.byte_capacity and self.buffer: self.byte_count -= len(self.buffer[0]) self.buffer.pop(0) self.forgot = True def format_buffered_logs_for_payload(): ''' Get the handler previously added to the root logger, and slurp buffered logs out of it to send to Healthchecks. ''' try: buffering_handler = next( handler for handler in logging.getLogger().handlers if isinstance(handler, Forgetful_buffering_handler) ) except StopIteration: # No handler means no payload. return '' payload = ''.join(message for message in buffering_handler.buffer) if buffering_handler.forgot: return PAYLOAD_TRUNCATION_INDICATOR + payload return payload def ping_monitor(ping_url_or_uuid, config_filename, state, dry_run): ''' Ping the given Healthchecks URL or UUID, modified with the monitor.State. Use the given configuration filename in any log entries. If this is a dry run, then don't actually ping anything. ''' if state is monitor.State.START: # Add a handler to the root logger that stores in memory the most recent logs emitted. That # way, we can send them all to Healthchecks upon a finish or failure state. logging.getLogger().addHandler(Forgetful_buffering_handler(PAYLOAD_LIMIT_BYTES)) payload = '' ping_url = ( ping_url_or_uuid if ping_url_or_uuid.startswith('http') else 'https://hc-ping.com/{}'.format(ping_url_or_uuid) ) dry_run_label = ' (dry run; not actually pinging)' if dry_run else '' healthchecks_state = MONITOR_STATE_TO_HEALTHCHECKS.get(state) if healthchecks_state: ping_url = '{}/{}'.format(ping_url, healthchecks_state) logger.info( '{}: Pinging Healthchecks {}{}'.format(config_filename, state.name.lower(), dry_run_label) ) logger.debug('{}: Using Healthchecks ping URL {}'.format(config_filename, ping_url)) if state in (monitor.State.FINISH, monitor.State.FAIL): payload = format_buffered_logs_for_payload() if not dry_run: logging.getLogger('urllib3').setLevel(logging.ERROR) requests.post(ping_url, data=payload)