apv

- 🐍 advanced python logging 📔
git clone git://git.acid.vegas/apv.git
Log | Files | Refs | Archive | README | LICENSE

commit 69efe86fb997769c55eaf1b2ca8e15f046b6b516
parent ccd9642784acc0c7fac2a1e1c6cf5c0f948f6e9a
Author: acidvegas <acid.vegas@acid.vegas>
Date: Tue, 27 May 2025 12:04:43 -0400

over simplified

Diffstat:
MREADME.md | 100++++++++++---------------------------------------------------------------------
Mapv/__init__.py | 8++++----
Mapv/apv.py | 269++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------------
Dapv/plugins/__init__.py | 2--
Dapv/plugins/cloudwatch.py | 101-------------------------------------------------------------------------------
Dapv/plugins/console.py | 71-----------------------------------------------------------------------
Dapv/plugins/file.py | 78------------------------------------------------------------------------------
Dapv/plugins/graylog.py | 59-----------------------------------------------------------
Msetup.py | 7++-----
Munit_test.py | 179++++++++++++++++++++++++++++++++++++++++---------------------------------------

10 files changed, 279 insertions(+), 595 deletions(-)

diff --git a/README.md b/README.md
@@ -14,12 +14,10 @@
     - [Console Logging with Details](#console-logging-with-details)
     - [File Logging with Rotation](#file-logging-with-rotation)
     - [File Logging with Compression and JSON Format](#file-logging-with-compression-and-json-format)
-    - [Graylog Integration](#graylog-integration)
-    - [AWS CloudWatch Integration](#aws-cloudwatch-integration)
     - [Mixing it all together](#mixing-it-all-together)
 
 ## Introduction
-APV emerged from a simple observation: despite the abundance of logging solutions, there's a glaring lack of standardization in application logging. As a developer deeply entrenched in Elasticsearch, AWS, and Graylog ecosystems, I found myself repeatedly grappling with inconsistent log formats and cumbersome integrations. APV is my response to this challenge – a logging library that doesn't aim to revolutionize the field, but rather to streamline it.
+APV emerged from a simple observation: despite the abundance of logging solutions, there's a glaring lack of standardization in application logging. APV is my response to this challenge – a logging library that doesn't aim to revolutionize the field, but rather to streamline it.
 
 ## Requirements
 - Python 3.10+
@@ -28,17 +26,7 @@ APV emerged from a simple observation: despite the abundance of logging solution
 
 ### From PyPI
 ```bash
-# Basic installation
 pip install apv
-
-# With CloudWatch support
-pip install apv[cloudwatch]
-
-# With ECS logging support
-pip install apv[ecs]
-
-# With all optional dependencies
-pip install "apv[cloudwatch,ecs]"
 ```
 
 ### From Source
@@ -53,34 +41,23 @@ pip install .
 - **File Logging**: Write logs to files with support for log rotation based on size and number of backups.
 - **Log Compression**: Automatically compress old log files using gzip to save disk space.
 - **JSON Logging**: Output logs in JSON format for better structure and integration with log management systems.
-- **ECS Logging**: Output logs in ECS format for better integration with [Elasticsearch](https://www.elastic.co/elasticsearch/)
 - **Detailed Log Messages**: Option to include module name, function name, and line number in log messages.
-- **Graylog Integration**: Send logs to a [Graylog](https://www.graylog.org/) server using GELF over UDP.
-- **AWS CloudWatch Integration**: Send logs to [AWS CloudWatch Logs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/logs/WhatIsCloudWatchLogs.html).
-- **Customizable Logging Levels**: Set the logging level to control verbosity.
 
 ## Configuration Options
 
 The `setup_logging` function accepts the following keyword arguments to customize logging behavior:
 
-| Name                     | Default                  | Description                                                                          |
-|--------------------------|--------------------------|--------------------------------------------------------------------------------------|
-| `level`                  | `INFO`                   | The logging level. *(`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`)*               |
-| `date_format`            | `'%Y-%m-%d %H:%M:%S'`    | The date format for log messages.                                                    |
-| `log_to_disk`            | `False`                  | Whether to log to disk.                                                              |
-| `max_log_size`           | `10*1024*1024` *(10 MB)* | The maximum size of log files before rotation *(in bytes)*.                          |
-| `max_backups`            | `7`                      | The maximum number of backup log files to keep.                                      |
-| `log_file_name`          | `'app'`                  | The base name of the log file.                                                       |
-| `json_log`               | `False`                  | Whether to log in JSON format.                                                       |
-| `ecs_log`                | `False`                  | Whether to log in ECS format.                                                        |
-| `show_details`           | `False`                  | Whether to include module name, function name, & line number in log messages.        |
-| `compress_backups`       | `False`                  | Whether to compress old log files using gzip.                                        |
-| `enable_graylog`         | `False`                  | Whether to enable logging to a Graylog server.                                       |
-| `graylog_host`           | `None`                   | The Graylog server host. *(Required if `enable_graylog` is `True`)*                  |
-| `graylog_port`           | `None`                   | The Graylog server port. *(Required if `enable_graylog` is `True`)*                  |
-| `enable_cloudwatch`      | `False`                  | Whether to enable logging to AWS CloudWatch Logs.                                    |
-| `cloudwatch_group_name`  | `None`                   | The name of the CloudWatch log group. *(Required if `enable_cloudwatch` is `True`)*  |
-| `cloudwatch_stream_name` | `None`                   | The name of the CloudWatch log stream. *(Required if `enable_cloudwatch` is `True`)* |
+| Name              | Default                  | Description                                                                   |
+|-------------------|--------------------------|-------------------------------------------------------------------------------|
+| `level`           | `INFO`                   | The logging level. *(`DEBUG`, `INFO`, `WARNING`, `ERROR`, `CRITICAL`)*        |
+| `date_format`     | `'%Y-%m-%d %H:%M:%S'`    | The date format for log messages.                                             |
+| `log_to_disk`     | `False`                  | Whether to log to disk.                                                       |
+| `max_log_size`    | `10*1024*1024` *(10 MB)* | The maximum size of log files before rotation *(in bytes)*.                   |
+| `max_backups`     | `7`                      | The maximum number of backup log files to keep.                               |
+| `log_file_name`   | `'app'`                  | The base name of the log file.                                                |
+| `json_log`        | `False`                  | Whether to log in JSON format.                                                |
+| `show_details`    | `False`                  | Whether to include module name, function name, & line number in log messages. |
+| `compress_backups`| `False`                  | Whether to compress old log files using gzip.                                 |
 
 ## Usage
 
@@ -147,53 +124,6 @@ apv.setup_logging(
 logging.debug('This is a debug message in JSON format.')
 ```
 
-### Graylog Integration
-
-```python
-import logging
-import apv
-
-# Set up logging to Graylog server
-apv.setup_logging(
-    level='INFO',
-    enable_graylog=True,
-    graylog_host='graylog.example.com',
-    graylog_port=12201
-)
-
-logging.info('This message will be sent to Graylog.')
-```
-
-### AWS CloudWatch Integration
-
-```python
-import logging
-import apv
-
-# Set up logging to AWS CloudWatch Logs
-apv.setup_logging(
-    level='INFO',
-    enable_cloudwatch=True,
-    cloudwatch_group_name='my_log_group',
-    cloudwatch_stream_name='my_log_stream'
-)
-
-logging.info('This message will be sent to AWS CloudWatch.')
-```
-
-### ECS Logging
-
-```python
-import logging
-import apv
-
-# Set up ECS logging
-apv.setup_logging(
-    level='INFO',
-    ecs_log=True
-)
-``` 
-
 ### Mixing it all together
 
 ```python
@@ -209,12 +139,6 @@ apv.setup_logging(
     log_file_name='app',
     json_log=True,
     compress_backups=True,
-    enable_graylog=True,
-    graylog_host='graylog.example.com',
-    graylog_port=12201,
-    enable_cloudwatch=True,
-    cloudwatch_group_name='my_log_group',
-    cloudwatch_stream_name='my_log_stream',
     show_details=True
 )
 ```
diff --git a/apv/__init__.py b/apv/__init__.py
@@ -1,4 +1,3 @@
-from .apv import * 
-
-__version__ = '1.0.4'
-__author__ = 'acidvegas' 
+#!/usr/bin/env python3
+# Advanced Python Logging - Developed by acidvegas in Python (https://git.acid.vegas/apv)
+# apv/__init__.py
+\ No newline at end of file
diff --git a/apv/apv.py b/apv/apv.py
@@ -2,143 +2,216 @@
 # Advanced Python Logging - Developed by acidvegas in Python (https://git.acid.vegas/apv)
 # apv.py
 
+import gzip
+import json
 import logging
 import logging.handlers
+import os
+import socket
 import sys
 
-sys.stdout.reconfigure(encoding='utf-8')
+
+class LogColors:
+    '''ANSI color codes for log messages'''
+
+    NOTSET    = '\033[97m'         # White text
+    DEBUG     = '\033[96m'         # Cyan
+    INFO      = '\033[92m'         # Green
+    WARNING   = '\033[93m'         # Yellow
+    ERROR     = '\033[91m'         # Red
+    CRITICAL  = '\033[97m\033[41m' # White on Red
+    FATAL     = '\033[97m\033[41m' # Same as CRITICAL
+    DATE      = '\033[90m'         # Dark Grey
+    MODULE    = '\033[95m'         # Pink
+    FUNCTION  = '\033[94m'         # Blue
+    LINE      = '\033[33m'         # Orange
+    RESET     = '\033[0m'
+    SEPARATOR = '\033[90m'         # Dark Grey
+
+
+class ConsoleFormatter(logging.Formatter):
+    '''A formatter for the consolethat supports colored output'''
+    
+    def __init__(self, datefmt: str = None, details: bool = False):
+        super().__init__(datefmt=datefmt)
+        self.details = details
+
+
+    def format(self, record: logging.LogRecord) -> str:
+        '''
+        Format a log record for the console
+        
+        :param record: The log record to format
+        '''
+
+        # Get the color for the log level
+        color = getattr(LogColors, record.levelname, LogColors.RESET)
+        
+        # Format the log level
+        log_level = f'{color}{record.levelname:<8}{LogColors.RESET}'
+
+        # Get the log message
+        message = record.getMessage()
+
+        # Format the timestamp
+        asctime = f'{LogColors.DATE}{self.formatTime(record, self.datefmt)}'
+
+        # Get the separator
+        separator = f'{LogColors.SEPARATOR} ┃ {LogColors.RESET}'
+        details   = f'{LogColors.MODULE}{record.module}{separator}{LogColors.FUNCTION}{record.funcName}{separator}{LogColors.LINE}{record.lineno}{separator}' if self.details else ''
+        
+        return f'{asctime}{separator}{log_level}{separator}{details}{message}'
+
+
+class JsonFormatter(logging.Formatter):
+    '''Formatter for JSON output'''
+    
+    def __init__(self, datefmt: str = None):
+        super().__init__(datefmt=datefmt)
+
+
+    def format(self, record: logging.LogRecord) -> str:
+        '''
+        Format a log record for JSON output
+        
+        :param record: The log record to format
+        '''
+
+        # Create a dictionary to store the log record
+        log_dict = {
+            '@timestamp'    : self.formatTime(record, self.datefmt),
+            'level'        : record.levelname,
+            'message'      : record.getMessage(),
+            'process_id'   : record.process,
+            'process_name' : record.processName,
+            'thread_id'    : record.thread,
+            'thread_name'  : record.threadName,
+            'logger_name'  : record.name,
+            'filename'     : record.filename,
+            'line_number'  : record.lineno,
+            'function'     : record.funcName,
+            'module'       : record.module,
+            'hostname'     : socket.gethostname()
+        }
+
+        # Add the exception if it exists
+        if record.exc_info:
+            log_dict['exception'] = self.formatException(record.exc_info)
+
+        # Add any custom attributes that start with an underscore
+        custom_attrs = {k: v for k, v in record.__dict__.items() if k.startswith('_') and not k.startswith('__')}
+        log_dict.update(custom_attrs)
+
+        return json.dumps(log_dict)
+
+
+class GZipRotatingFileHandler(logging.handlers.RotatingFileHandler):
+    '''RotatingFileHandler that compresses rotated log files'''
+    
+    def rotation_filename(self, default_name: str) -> str:
+        return default_name + '.gz'
+
+    def rotate(self, source: str, dest: str):
+        with open(source, 'rb') as src, gzip.open(dest, 'wb') as dst:
+            dst.write(src.read())
+
 
 class LoggerSetup:
-    def __init__(self, level='INFO', date_format='%Y-%m-%d %H:%M:%S',
-                 log_to_disk=False, max_log_size=10*1024*1024,
-                 max_backups=7, log_file_name='app', json_log=False,
-                 ecs_log=False, show_details=False, compress_backups=False,
-                 enable_graylog=False, graylog_host=None, graylog_port=None,
-                 enable_cloudwatch=False, cloudwatch_group_name=None, cloudwatch_stream_name=None):
+    def __init__(self, level: str = 'INFO', date_format: str = '%Y-%m-%d %H:%M:%S', log_to_disk: bool = False, max_log_size: int = 10*1024*1024, max_backups: int = 7, log_file_name: str = 'app', json_log: bool = False, show_details: bool = False, compress_backups: bool = False):
         '''
-        Initialize the LoggerSetup with provided parameters.
+        Initialize the LoggerSetup with provided parameters
         
-        :param level: The logging level (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL').
-        :param date_format: The date format for log messages.
-        :param log_to_disk: Whether to log to disk.
-        :param max_log_size: The maximum size of log files before rotation.
-        :param max_backups: The maximum number of backup log files to keep.
-        :param log_file_name: The base name of the log file.
-        :param json_log: Whether to log in JSON format.
-        :param show_details: Whether to show detailed log messages.
-        :param compress_backups: Whether to compress old log files using gzip.
-        :param enable_graylog: Whether to enable Graylog logging.
-        :param graylog_host: The Graylog host.
-        :param graylog_port: The Graylog port.
-        :param enable_cloudwatch: Whether to enable CloudWatch logging.
-        :param cloudwatch_group_name: The CloudWatch log group name.
-        :param cloudwatch_stream_name: The CloudWatch log stream name.
+        :param level: The logging level (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL')
+        :param date_format: The date format for log messages
+        :param log_to_disk: Whether to log to disk
+        :param max_log_size: The maximum size of log files before rotation
+        :param max_backups: The maximum number of backup log files to keep
+        :param log_file_name: The base name of the log file
+        :param json_log: Whether to log in JSON format
+        :param show_details: Whether to show detailed log messages
+        :param compress_backups: Whether to compress old log files using gzip
         '''
 
-        self.level                  = level
-        self.date_format            = date_format
-        self.log_to_disk            = log_to_disk
-        self.max_log_size           = max_log_size
-        self.max_backups            = max_backups
-        self.log_file_name          = log_file_name
-        self.json_log               = json_log
-        self.ecs_log                = ecs_log
-        self.show_details           = show_details
-        self.compress_backups       = compress_backups
-        self.enable_graylog         = enable_graylog
-        self.graylog_host           = graylog_host
-        self.graylog_port           = graylog_port
-        self.enable_cloudwatch      = enable_cloudwatch
-        self.cloudwatch_group_name  = cloudwatch_group_name
-        self.cloudwatch_stream_name = cloudwatch_stream_name
+        self.level            = level
+        self.date_format      = date_format
+        self.log_to_disk      = log_to_disk
+        self.max_log_size     = max_log_size
+        self.max_backups      = max_backups
+        self.log_file_name    = log_file_name
+        self.json_log         = json_log
+        self.show_details     = show_details
+        self.compress_backups = compress_backups
 
 
     def setup(self):
-        '''Set up logging with various handlers and options.'''
+        '''Set up logging with various handlers and options'''
 
         # Clear existing handlers
         logging.getLogger().handlers.clear()
-        logging.getLogger().setLevel(logging.DEBUG)  # Capture all logs at the root level
+        logging.getLogger().setLevel(logging.DEBUG)
 
         # Convert the level string to a logging level object
         level_num = getattr(logging, self.level.upper(), logging.INFO)
 
+        # Setup console handler
         self.setup_console_handler(level_num)
 
+        # Setup file handler if enabled
         if self.log_to_disk:
             self.setup_file_handler(level_num)
 
-        if self.enable_graylog:
-            self.setup_graylog_handler(level_num)
-
-        if self.enable_cloudwatch:
-            self.setup_cloudwatch_handler(level_num)
-
 
     def setup_console_handler(self, level_num: int):
-        '''Set up the console handler.'''
-        try:
-            from apv.plugins.console import setup_console_handler
-            setup_console_handler(level_num, self.date_format, self.show_details)
-        except ImportError:
-            logging.error('Failed to import console handler')
-
-
-    def setup_file_handler(self, level_num: int):
-        '''Set up the file handler.'''
-        try:
-            from apv.plugins.file import setup_file_handler
-            setup_file_handler(
-                level_num=level_num,
-                log_to_disk=self.log_to_disk,
-                max_log_size=self.max_log_size,
-                max_backups=self.max_backups,
-                log_file_name=self.log_file_name,
-                json_log=self.json_log,
-                ecs_log=self.ecs_log,
-                date_format=self.date_format,
-                compress_backups=self.compress_backups
-            )
-        except ImportError:
-            logging.error('Failed to import file handler')
-
-
-    def setup_graylog_handler(self, level_num: int):
         '''
-        Set up the Graylog handler.
+        Set up the console handler
         
-        :param level_num: The logging level number.
+        :param level_num: The logging level number
         '''
 
-        try:
-            from apv.plugins.graylog import setup_graylog_handler
-            setup_graylog_handler(level_num, self.graylog_host, self.graylog_port)
-        except ImportError:
-            logging.error('Failed to import Graylog handler')
+        # Create the console handler
+        console_handler = logging.StreamHandler()
+        console_handler.setLevel(level_num)
+        
+        # Create the formatter
+        formatter = JsonFormatter(datefmt=self.date_format) if self.json_log else ConsoleFormatter(datefmt=self.date_format, details=self.show_details)
+        console_handler.setFormatter(formatter)
+        
+        # Add the handler to the root logger
+        logging.getLogger().addHandler(console_handler)
 
 
-    def setup_cloudwatch_handler(self, level_num: int):
+    def setup_file_handler(self, level_num: int):
         '''
-        Set up the CloudWatch handler.
+        Set up the file handler
         
-        :param level_num: The logging level number.
+        :param level_num: The logging level number
         '''
 
-        try:
-            from apv.plugins.cloudwatch import setup_cloudwatch_handler
-            setup_cloudwatch_handler(
-                level_num,
-                self.cloudwatch_group_name,
-                self.cloudwatch_stream_name,
-                self.date_format
-            )
-        except ImportError:
-            logging.error('Failed to import CloudWatch handler')
+        # Create logs directory if it doesn't exist
+        logs_dir = os.path.join(sys.path[0], 'logs')
+        os.makedirs(logs_dir, exist_ok=True)
 
+        # Set up log file path
+        file_extension = '.json' if self.json_log else '.log'
+        log_file_path  = os.path.join(logs_dir, f'{self.log_file_name}{file_extension}')
+
+        # Create the rotating file handler
+        handler_class = GZipRotatingFileHandler if self.compress_backups else logging.handlers.RotatingFileHandler
+        file_handler  = handler_class(log_file_path, maxBytes=self.max_log_size, backupCount=self.max_backups)
+        file_handler.setLevel(level_num)
+
+        # Set up the appropriate formatter
+        formatter = JsonFormatter(datefmt=self.date_format) if self.json_log else logging.Formatter(fmt='%(asctime)s ┃ %(levelname)-8s ┃ %(module)s ┃ %(funcName)s ┃ %(lineno)d ┃ %(message)s', datefmt=self.date_format)
+        file_handler.setFormatter(formatter)
+
+        logging.getLogger().addHandler(file_handler)
 
 
 def setup_logging(**kwargs):
-    '''Set up logging with various handlers and options.'''
+    '''Set up logging with various handlers and options'''
 
+    # Create a LoggerSetup instance with the provided keyword arguments
     logger_setup = LoggerSetup(**kwargs)
-    logger_setup.setup()
-\ No newline at end of file
+
+    # Set up the logging system
+    logger_setup.setup() 
+\ No newline at end of file
diff --git a/apv/plugins/__init__.py b/apv/plugins/__init__.py
@@ -1 +0,0 @@
-# Empty file to make plugins a package 
-\ No newline at end of file
diff --git a/apv/plugins/cloudwatch.py b/apv/plugins/cloudwatch.py
@@ -1,100 +0,0 @@
-import logging
-import json
-import boto3
-from botocore.exceptions import ClientError
-
-class CloudWatchHandler(logging.Handler):
-    def __init__(self, group_name, stream_name):
-        super().__init__()
-        self.group_name = group_name
-        self.stream_name = stream_name
-        self.client = boto3.client('logs')
-        self._initialize_log_group_and_stream()
-
-    def _initialize_log_group_and_stream(self):
-        # Create log group if it doesn't exist
-        try:
-            self.client.create_log_group(logGroupName=self.group_name)
-        except ClientError as e:
-            if e.response['Error']['Code'] != 'ResourceAlreadyExistsException':
-                raise e
-
-        # Create log stream if it doesn't exist
-        try:
-            self.client.create_log_stream(
-                logGroupName=self.group_name,
-                logStreamName=self.stream_name
-            )
-        except ClientError as e:
-            if e.response['Error']['Code'] != 'ResourceAlreadyExistsException':
-                raise e
-
-    def _get_sequence_token(self):
-        try:
-            response = self.client.describe_log_streams(
-                logGroupName=self.group_name,
-                logStreamNamePrefix=self.stream_name,
-                limit=1
-            )
-            log_streams = response.get('logStreams', [])
-            return log_streams[0].get('uploadSequenceToken') if log_streams else None
-        except Exception:
-            return None
-
-    def emit(self, record):
-        try:
-            log_entry = self.format(record)
-            timestamp = int(record.created * 1000)
-            
-            event = {
-                'timestamp': timestamp,
-                'message': log_entry
-            }
-            
-            kwargs = {
-                'logGroupName': self.group_name,
-                'logStreamName': self.stream_name,
-                'logEvents': [event]
-            }
-            
-            sequence_token = self._get_sequence_token()
-            if sequence_token:
-                kwargs['sequenceToken'] = sequence_token
-                
-            self.client.put_log_events(**kwargs)
-        except Exception:
-            self.handleError(record) 
-
-def setup_cloudwatch_handler(level_num: int, group_name: str, stream_name: str, date_format: str):
-    '''Set up the CloudWatch handler.'''
-    try:
-        import boto3
-    except ImportError:
-        raise ImportError('boto3 is required for CloudWatch logging. (pip install boto3)')
-
-    if not group_name or not stream_name:
-        logging.error('CloudWatch log group and log stream must be specified for CloudWatch handler.')
-        return
-
-    cloudwatch_handler = CloudWatchHandler(group_name, stream_name)
-    cloudwatch_handler.setLevel(level_num)
-    
-    class JsonFormatter(logging.Formatter):
-        def format(self, record):
-            log_record = {
-                'time'        : self.formatTime(record, date_format),
-                'level'       : record.levelname,
-                'module'      : record.module,
-                'function'    : record.funcName,
-                'line'        : record.lineno,
-                'message'     : record.getMessage(),
-                'name'        : record.name,
-                'filename'    : record.filename,
-                'threadName'  : record.threadName,
-                'processName' : record.processName,
-            }
-            return json.dumps(log_record)
-    
-    cloudwatch_formatter = JsonFormatter(datefmt=date_format)
-    cloudwatch_handler.setFormatter(cloudwatch_formatter)
-    logging.getLogger().addHandler(cloudwatch_handler) 
-\ No newline at end of file
diff --git a/apv/plugins/console.py b/apv/plugins/console.py
@@ -1,70 +0,0 @@
-import logging
-
-class LogColors:
-    '''ANSI color codes for log messages.'''
-    RESET     = '\033[0m'
-    DATE      = '\033[90m'         # Dark Grey
-    DEBUG     = '\033[96m'         # Cyan
-    INFO      = '\033[92m'         # Green
-    WARNING   = '\033[93m'         # Yellow
-    ERROR     = '\033[91m'         # Red
-    CRITICAL  = '\033[97m\033[41m' # White on Red
-    FATAL     = '\033[97m\033[41m' # Same as CRITICAL
-    NOTSET    = '\033[97m'         # White text
-    SEPARATOR = '\033[90m'         # Dark Grey
-    MODULE    = '\033[95m'         # Pink
-    FUNCTION  = '\033[94m'         # Blue
-    LINE      = '\033[33m'         # Orange
-
-class ColoredFormatter(logging.Formatter):
-    def __init__(self, datefmt=None, show_details=False):
-        super().__init__(datefmt=datefmt)
-        self.show_details = show_details
-        self.LEVEL_COLORS = {
-            'NOTSET'   : LogColors.NOTSET,
-            'DEBUG'    : LogColors.DEBUG,
-            'INFO'     : LogColors.INFO,
-            'WARNING'  : LogColors.WARNING,
-            'ERROR'    : LogColors.ERROR,
-            'CRITICAL' : LogColors.CRITICAL,
-            'FATAL'    : LogColors.FATAL
-        }
-
-    def format(self, record):
-        log_level = record.levelname
-        message   = record.getMessage()
-        asctime   = self.formatTime(record, self.datefmt)
-        color     = self.LEVEL_COLORS.get(log_level, LogColors.RESET)
-        separator = f'{LogColors.SEPARATOR} ┃ {LogColors.RESET}'
-        
-        if self.show_details:
-            formatted = (
-                f'{LogColors.DATE}{asctime}{LogColors.RESET}'
-                f'{separator}'
-                f'{color}{log_level:<8}{LogColors.RESET}'
-                f'{separator}'
-                f'{LogColors.MODULE}{record.module}{LogColors.RESET}'
-                f'{separator}'
-                f'{LogColors.FUNCTION}{record.funcName}{LogColors.RESET}'
-                f'{separator}'
-                f'{LogColors.LINE}{record.lineno}{LogColors.RESET}'
-                f'{separator}'
-                f'{message}'
-            )
-        else:
-            formatted = (
-                f'{LogColors.DATE}{asctime}{LogColors.RESET}'
-                f'{separator}'
-                f'{color}{log_level:<8}{LogColors.RESET}'
-                f'{separator}'
-                f'{message}'
-            )
-        return formatted
-
-def setup_console_handler(level_num: int, date_format: str, show_details: bool):
-    '''Set up the console handler with colored output.'''
-    console_handler = logging.StreamHandler()
-    console_handler.setLevel(level_num)
-    console_formatter = ColoredFormatter(datefmt=date_format, show_details=show_details)
-    console_handler.setFormatter(console_formatter)
-    logging.getLogger().addHandler(console_handler) 
-\ No newline at end of file
diff --git a/apv/plugins/file.py b/apv/plugins/file.py
@@ -1,77 +0,0 @@
-import logging
-import logging.handlers
-import json
-import os
-import gzip
-
-class GZipRotatingFileHandler(logging.handlers.RotatingFileHandler):
-    '''RotatingFileHandler that compresses old log files using gzip.'''
-
-    def doRollover(self):
-        '''Compress old log files using gzip.'''
-        super().doRollover()
-        if self.backupCount > 0:
-            for i in range(self.backupCount, 0, -1):
-                sfn = f'{self.baseFilename}.{i}'
-                if os.path.exists(sfn):
-                    with open(sfn, 'rb') as f_in:
-                        with gzip.open(f'{sfn}.gz', 'wb') as f_out:
-                            f_out.writelines(f_in)
-                    os.remove(sfn)
-
-class JsonFormatter(logging.Formatter):
-    def __init__(self, date_format):
-        super().__init__()
-        self.date_format = date_format
-
-    def format(self, record):
-        log_record = {
-            'time'        : self.formatTime(record, self.date_format),
-            'level'       : record.levelname,
-            'module'      : record.module,
-            'function'    : record.funcName,
-            'line'        : record.lineno,
-            'message'     : record.getMessage(),
-            'name'        : record.name,
-            'filename'    : record.filename,
-            'threadName'  : record.threadName,
-            'processName' : record.processName,
-        }
-        return json.dumps(log_record)
-
-def setup_file_handler(level_num: int, log_to_disk: bool, max_log_size: int, 
-                      max_backups: int, log_file_name: str, json_log: bool,
-                      ecs_log: bool, date_format: str, compress_backups: bool):
-    '''Set up the file handler for logging to disk.'''
-    if not log_to_disk:
-        return
-
-    # Create 'logs' directory if it doesn't exist
-    logs_dir = os.path.join(os.getcwd(), 'logs')
-    os.makedirs(logs_dir, exist_ok=True)
-
-    # Use the specified log file name and set extension based on json_log
-    file_extension = '.json' if json_log else '.log'
-    log_file_path = os.path.join(logs_dir, f'{log_file_name}{file_extension}')
-
-    # Create the rotating file handler
-    handler_class = GZipRotatingFileHandler if compress_backups else logging.handlers.RotatingFileHandler
-    file_handler = handler_class(log_file_path, maxBytes=max_log_size, backupCount=max_backups)
-    file_handler.setLevel(level_num)
-
-    if ecs_log:
-        try:
-            import ecs_logging
-        except ImportError:
-            raise ImportError("The 'ecs-logging' library is required for ECS logging. Install it with 'pip install ecs-logging'.")
-        file_formatter = ecs_logging.StdlibFormatter()
-    elif json_log:
-        file_formatter = JsonFormatter(date_format)
-    else:
-        file_formatter = logging.Formatter(
-            fmt='%(asctime)s ┃ %(levelname)-8s ┃ %(module)s ┃ %(funcName)s ┃ %(lineno)d ┃ %(message)s',
-            datefmt=date_format
-        )
-
-    file_handler.setFormatter(file_formatter)
-    logging.getLogger().addHandler(file_handler) 
-\ No newline at end of file
diff --git a/apv/plugins/graylog.py b/apv/plugins/graylog.py
@@ -1,58 +0,0 @@
-import logging
-import json
-import socket
-import zlib
-
-class GraylogHandler(logging.Handler):
-    def __init__(self, host, port):
-        super().__init__()
-        self.host = host
-        self.port = port
-        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
-        
-        # Mapping from Python logging levels to Graylog (syslog) levels
-        self.level_mapping = {
-            logging.CRITICAL : 2, # Critical
-            logging.ERROR    : 3, # Error
-            logging.WARNING  : 4, # Warning
-            logging.INFO     : 6, # Informational
-            logging.DEBUG    : 7, # Debug
-            logging.NOTSET   : 7  # Default to Debug
-        }
-
-    def emit(self, record):
-        try:
-            log_entry = self.format(record)
-            graylog_level = self.level_mapping.get(record.levelno, 7)
-            
-            gelf_message = {
-                'version'       : '1.1',
-                'host'          : socket.gethostname(),
-                'short_message' : record.getMessage(),
-                'full_message'  : log_entry,
-                'timestamp'     : record.created,
-                'level'         : graylog_level,
-                '_logger_name'  : record.name,
-                '_file'         : record.pathname,
-                '_line'         : record.lineno,
-                '_function'     : record.funcName,
-                '_module'       : record.module,
-            }
-            
-            message = json.dumps(gelf_message).encode('utf-8')
-            compressed = zlib.compress(message)
-            self.sock.sendto(compressed, (self.host, self.port))
-        except Exception:
-            self.handleError(record) 
-
-def setup_graylog_handler(level_num: int, graylog_host: str, graylog_port: int):
-    '''Set up the Graylog handler.'''
-    if graylog_host is None or graylog_port is None:
-        logging.error('Graylog host and port must be specified for Graylog handler.')
-        return
-
-    graylog_handler = GraylogHandler(graylog_host, graylog_port)
-    graylog_handler.setLevel(level_num)
-    graylog_formatter = logging.Formatter(fmt='%(message)s')
-    graylog_handler.setFormatter(graylog_formatter)
-    logging.getLogger().addHandler(graylog_handler) 
-\ No newline at end of file
diff --git a/setup.py b/setup.py
@@ -4,12 +4,13 @@
 
 from setuptools import setup, find_packages
 
+
 with open('README.md', 'r', encoding='utf-8') as fh:
 	long_description = fh.read()
 
 setup(
     name='apv',
-    version='1.0.4',
+    version='4.0.0',
     description='Advanced Python Logging',
     author='acidvegas',
     author_email='acid.vegas@acid.vegas',
@@ -25,10 +26,6 @@ setup(
     install_requires=[
         # No required dependencies for basic functionality
     ],
-    extras_require={
-        'cloudwatch': ['boto3'],
-        'ecs' : ['ecs-logging'],
-    },
     classifiers=[
         'Programming Language :: Python :: 3',
         'License :: OSI Approved :: ISC License (ISCL)',
diff --git a/unit_test.py b/unit_test.py
@@ -1,95 +1,95 @@
-#! /usr/bin/env python3
+#!/usr/bin/env python3
 # Advanced Python Logging - Developed by acidvegas in Python (https://git.acid.vegas/apv)
-# unittest.py
+# unit_test.py
 
 import logging
+import os
 import random
+import sys
 import time
 
-# prevent bytecode files (.pyc) from being written
-from sys import dont_write_bytecode
-dont_write_bytecode = True
-
-import apv
-
-# Test console logging with custom date format
-apv.setup_logging(level='DEBUG', date_format='%H:%M:%S')
-logging.debug('Testing debug message in console.')
-logging.info('Testing info message in console.')
-logging.warning('Testing warning message in console.')
-logging.error('Testing error message in console.')
-logging.critical('Testing critical message in console.')
-
-print()
-
-# Test console logging with details
-time.sleep(2)
-apv.setup_logging(level='DEBUG', date_format='%Y-%m-%d %H:%M:%S', show_details=True)
-logging.debug('Testing debug message in console with details.')
-logging.info('Testing info message in console with details.')
-logging.warning('Testing warning message in console with details.')
-logging.error('Testing error message in console with details.')
-logging.critical('Testing critical message in console with details.')
-
-print()
-
-# Test disk logging with JSON and regular rotation
-logging.debug('Starting test: Disk logging with JSON and regular rotation...')
-time.sleep(2)
-apv.setup_logging(level='DEBUG', log_to_disk=True, max_log_size=1024, max_backups=3, log_file_name='json_log', json_log=True, show_details=True)
-for i in range(100):
-    log_level = random.choice([logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL])
-    logging.log(log_level, f'Log entry {i+1} for JSON & regular rotation test.')
-    time.sleep(0.1)
-
-print()
-
-# Test disk logging with rotation & compression
-logging.debug('Starting test: Disk logging with rotation & compression...')
-time.sleep(2)
-apv.setup_logging(level='DEBUG', log_to_disk=True, max_log_size=1024, max_backups=3, log_file_name='plain_log', show_details=True, compress_backups=True)
-for i in range(100):
-    log_level = random.choice([logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL])
-    logging.log(log_level, f'Log entry {i+1} for disk rotation & compression test.')
-    time.sleep(0.1)
-
-logging.info('Test completed. Check the logs directory for disk logging & JSON logging tests.')
-
-print()
-
-try:
-    import ecs_logging
-except ImportError:
-    pass
-else:
-    # Test ECS logging
-    logging.debug('Starting test: ECS logging...')
-    time.sleep(2)
-    apv.setup_logging(level='DEBUG', ecs_log=True)
-    logging.debug('This is a test log message to ECS.')
-    logging.info('This is a test log message to ECS.')
-    logging.warning('This is a test log message to ECS.')
-    logging.error('This is a test log message to ECS.')
-    logging.critical('This is a test log message to ECS.')
-
-print()
-
-# Test Graylog handler (Uncomment & configure to test)
-# logging.debug('Starting test: Graylog handler...')
-# time.sleep(2)
-# apv.setup_logging(level='DEBUG', enable_graylog=True, graylog_host='your_graylog_host', graylog_port=12201)
-# logging.debug('This is a test log message to Graylog.')
-# logging.info('This is a test log message to Graylog.')
-# logging.warning('This is a test log message to Graylog.')
-# logging.error('This is a test log message to Graylog.')
-# logging.critical('This is a test log message to Graylog.')
-
-# Test CloudWatch handler (Uncomment & configure to test)
-# logging.debug('Starting test: CloudWatch handler...')
-# time.sleep(2)
-# apv.setup_logging(level='DEBUG', enable_cloudwatch=True, cloudwatch_group_name='your_log_group', cloudwatch_stream_name='your_log_stream')
-# logging.debug('This is a test log message to CloudWatch.')
-# logging.info('This is a test log message to CloudWatch.')
-# logging.warning('This is a test log message to CloudWatch.')
-# logging.error('This is a test log message to CloudWatch.')
-# logging.critical('This is a test log message to CloudWatch.')
+sys.dont_write_bytecode = True # FUCKOFF __pycache__
+
+import apv.apv as apv
+
+
+def test_console_logging():
+	'''Test basic console logging functionality'''
+
+	print('\nTesting Console Logging...')
+	apv.setup_logging(level='DEBUG', date_format='%H:%M:%S')
+	for level in ['debug', 'info', 'warning', 'error', 'critical']:
+		getattr(logging, level)(f'Testing {level} message in console.')
+	time.sleep(1)
+
+
+def test_json_console_logging():
+	'''Test JSON console logging'''
+
+	print('\nTesting JSON Console Logging...')
+	apv.setup_logging(level='DEBUG', date_format='%H:%M:%S', json_log=True, log_to_disk=False)
+	logging.info('Test JSON console message with custom field', extra={'_custom_field': 'test value'})
+	logging.warning('Test JSON console warning with error', exc_info=Exception('Test error'))
+	time.sleep(1)
+
+
+def test_detailed_logging():
+	'''Test console logging with details'''
+
+	print('\nTesting Detailed Logging...')
+	apv.setup_logging(level='DEBUG', show_details=True)
+	for level in ['debug', 'info', 'warning', 'error', 'critical']:
+		getattr(logging, level)(f'Testing {level} message with details.')
+	time.sleep(1)
+
+
+def test_file_logging():
+	'''Test file logging with rotation'''
+
+	print('\nTesting File Logging...')
+	log_file = 'logs/test_log.log'
+	apv.setup_logging(level='DEBUG', log_to_disk=True, max_log_size=1024, max_backups=3, log_file_name='test_log')
+	for i in range(50):
+		level = random.choice(['debug', 'info', 'warning', 'error', 'critical'])
+		getattr(logging, level)(f'File logging test message {i}')
+	
+	assert os.path.exists(log_file), "Log file was not created"
+	time.sleep(1)
+
+
+def test_json_logging():
+	'''Test JSON format logging'''
+
+	print('\nTesting JSON Logging...')
+	apv.setup_logging(level='DEBUG', log_to_disk=True, log_file_name='json_test', json_log=True)
+	logging.info('Test JSON formatted log message')
+	assert os.path.exists('logs/json_test.json'), "JSON log file was not created"
+	time.sleep(1)
+
+
+def test_compressed_logging():
+	'''Test compressed log files'''
+
+	print('\nTesting Compressed Logging...')
+	apv.setup_logging(level='DEBUG', log_to_disk=True, max_log_size=512, max_backups=2, log_file_name='compressed_test', compress_backups=True)
+	for i in range(100):
+		logging.info(f'Testing compression message {i}')
+	time.sleep(1)
+	# Check for .gz files
+	gz_files = [f for f in os.listdir('logs') if f.startswith('compressed_test') and f.endswith('.gz')]
+	assert len(gz_files) > 0, 'No compressed log files were created'
+
+
+if __name__ == '__main__':
+	# Create logs directory if it doesn't exist
+	os.makedirs('logs', exist_ok=True)
+	
+	# Run all tests
+	test_console_logging()
+	test_json_console_logging()
+	test_detailed_logging()
+	test_file_logging()
+	test_json_logging()
+	test_compressed_logging()
+
+	print('\nAll tests completed successfully!')
+\ No newline at end of file