import logging import logging.handlers import requests import traceback import datetime import json from threading import Event, Thread, Condition, Lock, enumerate from time import sleep class LogzioHandler(logging.Handler): # Hold all logs buffered logs = [] # Event for locking buffer additions while draining buffer_event = Event() # Condition to count log messages logs_counter_condition = Condition() # Lock to only drain logs once drain_lock = Lock() def __init__(self, token, logs_drain_count=100, logs_drain_timeout=10, logzio_type="python", url="https://listener.logz.io:8071/"): if token is "": raise Exception("Logz.io Token must be provided") logging.Handler.__init__(self) self.logs_drain_count = logs_drain_count self.logs_drain_timeout = logs_drain_timeout self.logzio_type = logzio_type self.url = "{0}?token={1}".format(url, token) self.is_main_thread_active = lambda: any((i.name == "MainThread") and i.is_alive() for i in enumerate()) self.buffer_event.set() # Create threads timeout_thread = Thread(target=self.wait_to_timeout_and_drain) counter_thread = Thread(target=self.count_logs_and_drain) # And start them timeout_thread.start() counter_thread.start() def wait_to_timeout_and_drain(self): while True: sleep(self.logs_drain_timeout) if len(self.logs) > 0: self.drain_messages() if not self.is_main_thread_active(): # Signal the counter thread so it would exit as well try: self.logs_counter_condition.acquire() self.logs_counter_condition.notify() finally: self.logs_counter_condition.release() break def count_logs_and_drain(self): try: # Acquire the condition self.logs_counter_condition.acquire() # Running indefinite while True: # Waiting for new log lines to come self.logs_counter_condition.wait() if not self.is_main_thread_active(): break # Do we have enough logs to drain? if len(self.logs) >= self.logs_drain_count: self.drain_messages() finally: self.logs_counter_condition.release() def add_to_buffer(self, message): # Check if we are currently draining buffer so we wont loose logs self.buffer_event.wait() try: # Acquire the condition self.logs_counter_condition.acquire() self.logs.append(json.dumps(message)) # Notify watcher for a new log coming in self.logs_counter_condition.notify() finally: # Release the condition self.logs_counter_condition.release() def handle_exceptions(self, message): if message.exc_info: return '\n'.join(traceback.format_exception(*message.exc_info)) else: return message.getMessage() def format_message(self, message): message_field = self.handle_exceptions(message) now = datetime.datetime.utcnow() timestamp = now.strftime("%Y-%m-%dT%H:%M:%S") + ".%03d" % (now.microsecond / 1000) + "Z" return_json = { "logger": message.name, "line_number": message.lineno, "path_name": message.pathname, "log_level": message.levelname, "message": message_field, "type": self.logzio_type, "@timestamp": timestamp } return return_json def drain_messages(self): try: self.buffer_event.clear() self.drain_lock.acquire() # Not configurable from the outside sleep_between_retries = 2000 number_of_retries = 4 success_in_send = False headers = {"Content-type": "text/plain"} for current_try in range(number_of_retries): response = requests.post(self.url, headers=headers, data='\n'.join(self.logs)) if response.status_code != 200: sleep(sleep_between_retries) sleep_between_retries *= 2 else: success_in_send = True break if success_in_send: # Only clear the logs from the buffer if we managed to send them # Clear the buffer self.logs = [] finally: self.buffer_event.set() self.drain_lock.release() def emit(self, record): self.add_to_buffer(self.format_message(record))