justinooo icon

Python - Logging Module v1.1.0 (logging.py)

justinooo | PRO | 01/20/25 11:46:07 PM UTC (Edited) | 0 ⭐ | 1023 👁️ | Never ⏰ | [Logging, Betterstack]
Python |

5.53 KB

|

Source Code

|

0 👍

/

0 👎

"""
File: logging.py
Author: Justin Garofolo
Date: 2024-05-31
Version: 1.1.0
 
Description:
    A flexible logging system for Python applications.
    It supports multiple logging levels and asynchronous log handling through threaded workers.
    The module can be easily integrated into projects with the capability to log messages both locally and remotely.
    Note that remote logging capabilities are implemented via BetterStack's logging service by default: https://betterstack.com/logs
 
Copyright:
    Copyright (C) 2024 SneakSync. All rights reserved.
 
Documentation:
    https://link.sneaksync.com/logging
"""
import requests, os, threading
from typing import Optional
from enum import Enum
from datetime import datetime, timezone
from http import HTTPStatus
from dataclasses import dataclass
from queue import Queue
 
def create_logger_from_env(
    name: str,
    env_var: str,
    auto_print: bool = True
) -> "Logger":
    tk = os.getenv(env_var)
    logger = Logger(name, token = tk, auto_print = auto_print)
    if not tk:
        logger.warn(f"Failed to initialize instance of Logger using create_logger_from_env. Missing environment variable: {env_var}")
    return logger
 
def create_logger(
    name: str,
    token: Optional[str] = None,
    auto_print: bool = True
) -> "Logger":
    return Logger(name, token = token, auto_print = auto_print)
 
class Level(Enum):
    TRACE = "trace"
    DEBUG = "debug"
    INFO = "info"
    WARN = "warn"
    ERROR = "error"
    FATAL = "fatal"
    PANIC = "panic"
 
@dataclass
class Log:
    id: int
    timestamp: datetime
    message: str
    level: Level
 
class Logger:
    _inc: int = 0 # auto-increment for unique log ID
 
    def __init__(
        self,
        name: str,
        token: Optional[str] = None,
        worker_count: int = 3,
        auto_print: bool = True
    ):
        self.name = name
        self.token = token
        self.worker_count = worker_count
        self.auto_print = auto_print
        self.queue = Queue()
    
        if token:
            mask = ('*' * (len(token) - 4) + token[-4:]) \
                if len(token) > 4 else token
            self.debug(f"Initialized logging module with token: {mask}")
 
    def log_worker(self, worker_id: int):
        self.info(f"Starting log worker (ID: {worker_id})")
        while True:
            log = self.queue.get()
 
            # thread kill switch
            if log is None:
                break
           
            if not isinstance(log, Log):
                type_str = type(log).__name__
                print(f"Item from log queue is not an instance of Log: {log} [{type_str}]")
                continue
 
            self.upload_log(log)
            self.queue.task_done()
        self.info(f"Killed log worker (ID: {worker_id})")
    
    def start_workers(self):
        for idx in range(self.worker_count):
            worker_id = idx + 1
            worker = threading.Thread(target = self.log_worker, args = (worker_id,))
            worker.daemon = True
            worker.start()
 
    def stop_workers(self):
        for _ in range(self.worker_count):
            self.queue.put(None)
 
    def log(self, level: Level, message: str):
        Logger._inc += 1
        log_id = Logger._inc
 
        if self.auto_print:
            now = datetime.now()
            now_str = now.strftime('%Y-%m-%d %I:%M:%S %p')
            print(f"[{self.name} | {now_str} | {level.value}] {message}")
 
        log_obj = Log(
            id = log_id,
            timestamp = datetime.now(timezone.utc),
            message = message,
            level = level
        )
        self.queue.put(log_obj)
 
    def debug(self, message: str):
        return self.log(Level.DEBUG, message)
 
    def info(self, message: str):
        return self.log(Level.INFO, message)
 
    def warn(self, message: str):
        return self.log(Level.WARN, message)
 
    def error(self, message: str):
        return self.log(Level.ERROR, message)
 
    def upload_log(self, log: Log) -> bool:
        headers = {
            'Content-Type': 'application/json',
            'Authorization': f"Bearer {self.token}"
        }
        data = {
            "dt": log.timestamp.strftime('%Y-%m-%d %H:%M:%S.%f UTC'),
            # "message": f"[#{log.id}] {log.message}",
            "message": log.message,
            "level": log.level.value
        }
        url = "https://in.logs.betterstack.com"
        response = requests.post(url, json = data, headers = headers)
        if response.status_code != HTTPStatus.ACCEPTED.value:
            print(f"Failed to send log to logtail, status code: {response.status_code}")
            return False
        return True
 
__default_logger: Optional[Logger] = None
 
def set_default(logger: Logger):
    global __default_logger
    __default_logger = logger
 
def debug(message: str):
    assert __default_logger is not None, "Failed to invoke 'logging.debug': missing default logger."
    return __default_logger.debug(message)
 
def info(message: str):
    assert __default_logger is not None, "Failed to invoke 'logging.info': missing default logger."
    return __default_logger.info(message)
 
def warn(message: str):
    assert __default_logger is not None, "Failed to invoke 'logging.warn': missing default logger."
    return __default_logger.warn(message)
 
def error(message: str):
    assert __default_logger is not None, "Failed to invoke 'logging.error': missing default logger."
    return __default_logger.error(message)

Comments