Advertisement
justinooo

Python - Logging Module (logging.py)

May 31st, 2024 (edited)
619
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 5.53 KB | Source Code | 0 0
  1. """
  2. File: logging.py
  3. Author: Justin Garofolo
  4. Date: 2024-05-31
  5. Version: 1.1.0
  6.  
  7. Description:
  8.    A flexible logging system for Python applications.
  9.    It supports multiple logging levels and asynchronous log handling through threaded workers.
  10.    The module can be easily integrated into projects with the capability to log messages both locally and remotely.
  11.    Note that remote logging capabilities are implemented via BetterStack's logging service by default: https://betterstack.com/logs
  12.  
  13. Copyright:
  14.    Copyright (C) 2024 SneakSync. All rights reserved.
  15.  
  16. Documentation:
  17.    https://link.sneaksync.com/logging
  18. """
  19. import requests, os, threading
  20. from typing import Optional
  21. from enum import Enum
  22. from datetime import datetime, timezone
  23. from http import HTTPStatus
  24. from dataclasses import dataclass
  25. from queue import Queue
  26.  
  27. def create_logger_from_env(
  28.     name: str,
  29.     env_var: str,
  30.     auto_print: bool = True
  31. ) -> "Logger":
  32.     tk = os.getenv(env_var)
  33.     logger = Logger(name, token = tk, auto_print = auto_print)
  34.     if not tk:
  35.         logger.warn(f"Failed to initialize instance of Logger using create_logger_from_env. Missing environment variable: {env_var}")
  36.     return logger
  37.  
  38. def create_logger(
  39.     name: str,
  40.     token: Optional[str] = None,
  41.     auto_print: bool = True
  42. ) -> "Logger":
  43.     return Logger(name, token = token, auto_print = auto_print)
  44.  
  45. class Level(Enum):
  46.     TRACE = "trace"
  47.     DEBUG = "debug"
  48.     INFO = "info"
  49.     WARN = "warn"
  50.     ERROR = "error"
  51.     FATAL = "fatal"
  52.     PANIC = "panic"
  53.  
  54. @dataclass
  55. class Log:
  56.     id: int
  57.     timestamp: datetime
  58.     message: str
  59.     level: Level
  60.  
  61. class Logger:
  62.     _inc: int = 0 # auto-increment for unique log ID
  63.  
  64.     def __init__(
  65.         self,
  66.         name: str,
  67.         token: Optional[str] = None,
  68.         worker_count: int = 3,
  69.         auto_print: bool = True
  70.     ):
  71.         self.name = name
  72.         self.token = token
  73.         self.worker_count = worker_count
  74.         self.auto_print = auto_print
  75.         self.queue = Queue()
  76.    
  77.         if token:
  78.             mask = ('*' * (len(token) - 4) + token[-4:]) \
  79.                 if len(token) > 4 else token
  80.             self.debug(f"Initialized logging module with token: {mask}")
  81.  
  82.     def log_worker(self, worker_id: int):
  83.         self.info(f"Starting log worker (ID: {worker_id})")
  84.         while True:
  85.             log = self.queue.get()
  86.  
  87.             # thread kill switch
  88.             if log is None:
  89.                 break
  90.            
  91.             if not isinstance(log, Log):
  92.                 type_str = type(log).__name__
  93.                 print(f"Item from log queue is not an instance of Log: {log} [{type_str}]")
  94.                 continue
  95.  
  96.             self.upload_log(log)
  97.             self.queue.task_done()
  98.         self.info(f"Killed log worker (ID: {worker_id})")
  99.    
  100.     def start_workers(self):
  101.         for idx in range(self.worker_count):
  102.             worker_id = idx + 1
  103.             worker = threading.Thread(target = self.log_worker, args = (worker_id,))
  104.             worker.daemon = True
  105.             worker.start()
  106.  
  107.     def stop_workers(self):
  108.         for _ in range(self.worker_count):
  109.             self.queue.put(None)
  110.  
  111.     def log(self, level: Level, message: str):
  112.         Logger._inc += 1
  113.         log_id = Logger._inc
  114.  
  115.         if self.auto_print:
  116.             now = datetime.now()
  117.             now_str = now.strftime('%Y-%m-%d %I:%M:%S %p')
  118.             print(f"[{self.name} | {now_str} | {level.value}] {message}")
  119.  
  120.         log_obj = Log(
  121.             id = log_id,
  122.             timestamp = datetime.now(timezone.utc),
  123.             message = message,
  124.             level = level
  125.         )
  126.         self.queue.put(log_obj)
  127.  
  128.     def debug(self, message: str):
  129.         return self.log(Level.DEBUG, message)
  130.  
  131.     def info(self, message: str):
  132.         return self.log(Level.INFO, message)
  133.  
  134.     def warn(self, message: str):
  135.         return self.log(Level.WARN, message)
  136.  
  137.     def error(self, message: str):
  138.         return self.log(Level.ERROR, message)
  139.  
  140.     def upload_log(self, log: Log) -> bool:
  141.         headers = {
  142.             'Content-Type': 'application/json',
  143.             'Authorization': f"Bearer {self.token}"
  144.         }
  145.         data = {
  146.             "dt": log.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f UTC'),
  147.             # "message": f"[#{log.id}] {log.message}",
  148.             "message": log.message,
  149.             "level": log.level.value
  150.         }
  151.         url = "https://in.logs.betterstack.com"
  152.         response = requests.post(url, json = data, headers = headers)
  153.         if response.status_code != HTTPStatus.ACCEPTED.value:
  154.             print(f"Failed to send log to logtail, status code: {response.status_code}")
  155.             return False
  156.         return True
  157.  
  158. __default_logger: Optional[Logger] = None
  159.  
  160. def set_default(logger: Logger):
  161.     global __default_logger
  162.     __default_logger = logger
  163.  
  164. def debug(message: str):
  165.     assert __default_logger is not None, "Failed to invoke 'logging.debug': missing default logger."
  166.     return __default_logger.debug(message)
  167.  
  168. def info(message: str):
  169.     assert __default_logger is not None, "Failed to invoke 'logging.info': missing default logger."
  170.     return __default_logger.info(message)
  171.  
  172. def warn(message: str):
  173.     assert __default_logger is not None, "Failed to invoke 'logging.warn': missing default logger."
  174.     return __default_logger.warn(message)
  175.  
  176. def error(message: str):
  177.     assert __default_logger is not None, "Failed to invoke 'logging.error': missing default logger."
  178.     return __default_logger.error(message)
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement