From 6bb197962b15a10e23d556ae23bef0dcdb6a8a68 Mon Sep 17 00:00:00 2001 From: Enes Esvet Kuzucu Date: Thu, 19 Sep 2024 20:55:02 +0300 Subject: [PATCH] Stable version --- README.md | 270 +++++++++++++++++++++++++---- indented_logger/indented_logger.py | 209 ++++++++++++++++++++-- setup.py | 2 +- 3 files changed, 438 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index d7e15d5..96f1d2d 100644 --- a/README.md +++ b/README.md @@ -1,58 +1,264 @@ -# What is IndentedLogger? +# IndentedLogger -**IndentedLogger** is a very simple wrapper around Python's standard logging package that adds indentation support to your logs. It allows you to visually represent the hierarchical structure or depth of your logging messages, making it easier to understand the flow of execution in complex systems. +IndentedLogger is a powerful yet simple wrapper around Python's standard `logging` package that adds automatic indentation and enhanced formatting support to your logs. It visually represents the hierarchical structure and depth of your logging messages, making it easier to understand the flow of execution in complex systems. -## Usage and Output +## Table of Contents -To get output like this : -```python -2024-08-15 12:34:56 - INFO - Starting process -2024-08-15 12:34:56 - INFO - Loading data -2024-08-15 12:34:56 - INFO - Processing data -2024-08-15 12:34:56 - INFO - Saving results -2024-08-15 12:34:56 - INFO - Process complete +- [Features](#features) +- [Installation](#installation) +- [Usage](#usage) + - [Basic Setup](#basic-setup) + - [Automatic Indentation with Decorators](#automatic-indentation-with-decorators) + - [Manual Indentation](#manual-indentation) + - [Including or Excluding Function Names](#including-or-excluding-function-names) + - [Aligning Function Names at a Specific Column](#aligning-function-names-at-a-specific-column) + - [Message Truncation (Optional)](#message-truncation-optional) +- [Benefits](#benefits) +- [License](#license) + +## Features + +- **Automatic Indentation via Decorators**: Use decorators to automatically manage indentation levels based on function call hierarchy. +- **Manual Indentation Support**: Add manual indentation to specific log messages for granular control. +- **Custom Formatter and Logger**: Includes an `IndentFormatter` and a custom logger class that handle indentation and formatting seamlessly. +- **Optional Function Names**: Choose to include or exclude function names in your log messages. +- **Function Name Alignment**: Align function names at a specified column for consistent and readable logs. +- **Message Truncation (Optional)**: Optionally truncate long messages to a specified length. +- **Thread Safety**: Manages indentation levels per thread, ensuring correct behavior in multi-threaded applications. +- **Easy Integration**: Seamlessly integrates with existing logging setups with minimal changes. + +## Installation +You can install IndentedLogger via pip: + +```bash +pip install indented_logger ``` +*Note: Ensure that your Python version is **3.8** or higher to utilize all features effectively.* + +## Usage + +### Basic Setup + ```python from indented_logger import IndentedLogger import logging # Setup the logger -logger_setup = IndentedLogger(name='my_logger', level=logging.INFO, log_file='app.log') +logger_setup = IndentedLogger(name='my_logger', level=logging.INFO) +logger = logger_setup.get_logger() + +# Basic logging +logger.info('Starting process') +logger.info('Process complete') +``` + +### Automatic Indentation with Decorators + +Use the `@log_indent` decorator to automatically manage indentation levels based on function calls. + +```python +from indented_logger import IndentedLogger, log_indent +import logging + +# Setup the logger with function names included +logger_setup = IndentedLogger(name='my_logger', level=logging.INFO, include_func=True) logger = logger_setup.get_logger() -# Log messages with different indentation levels -logger.info('Starting process', extra={'lvl': 0}) -logger.info('Loading data', extra={'lvl': 1}) -logger.info('Processing data', extra={'lvl': 2}) -logger.info('Saving results', extra={'lvl': 1}) -logger.info('Process complete', extra={'lvl': 0}) +@log_indent +def start_process(): + logger.info('Starting process') + load_data() + process_data() + logger.info('Process complete') +@log_indent +def load_data(): + logger.info('Loading data') + +@log_indent +def process_data(): + logger.info('Processing data') + +start_process() ``` -## Installation -You can install IndentedLogger via pip: +**Output:** -```bash -pip install indented_logger +``` +2024-08-15 12:34:56 - INFO - Entering function: start_process {start_process} +2024-08-15 12:34:56 - INFO - Starting process {start_process} +2024-08-15 12:34:56 - INFO - Entering function: load_data {load_data} +2024-08-15 12:34:56 - INFO - Loading data {load_data} +2024-08-15 12:34:56 - INFO - Exiting function: load_data {load_data} +2024-08-15 12:34:56 - INFO - Entering function: process_data {process_data} +2024-08-15 12:34:56 - INFO - Processing data {process_data} +2024-08-15 12:34:56 - INFO - Exiting function: process_data {process_data} +2024-08-15 12:34:56 - INFO - Process complete {start_process} +2024-08-15 12:34:56 - INFO - Exiting function: start_process {start_process} ``` +### Manual Indentation -## Benefits of Using IndentedLogger +You can manually adjust indentation levels using the `lvl` parameter in logging calls. -- **Simplicity**: It’s a minimalistic wrapper that adds just what you need—indentation—without altering the core logging functionalities you're familiar with. -- **Enhanced Readability**: The added indentation levels make it easy to follow the hierarchy or depth of operations in your logs. -- **Organized Logs**: Indentation allows you to group related log messages visually, making it easier to understand nested processes. -- **Easy Integration**: Seamlessly integrates with existing logging setups, requiring minimal changes to your current logging configuration. +```python +# Manual indentation +logger.info('Starting process', lvl=0) +logger.info('Loading data', lvl=1) +logger.info('Processing data', lvl=2) +logger.info('Saving results', lvl=1) +logger.info('Process complete', lvl=0) +``` -## Features +**Output:** + +``` +2024-08-15 12:34:56 - INFO - Starting process +2024-08-15 12:34:56 - INFO - Loading data +2024-08-15 12:34:56 - INFO - Processing data +2024-08-15 12:34:56 - INFO - Saving results +2024-08-15 12:34:56 - INFO - Process complete +``` + +### Including or Excluding Function Names + +Include or exclude function names in your log messages by setting the `include_func` parameter when initializing `IndentedLogger`. + +```python +# Include function names +logger_setup = IndentedLogger(name='my_logger', level=logging.INFO, include_func=True) + +# Exclude function names +logger_setup = IndentedLogger(name='my_logger', level=logging.INFO, include_func=False) +``` + +### Aligning Function Names at a Specific Column + +You can align function names at a specific column using the `min_func_name_col` parameter. This ensures that the function names start at the same column in each log entry, improving readability. + +```python +# Setup the logger with function names included and alignment at column 80 +logger_setup = IndentedLogger( + name='my_logger', + level=logging.INFO, + include_func=True, + min_func_name_col=80 +) +logger = logger_setup.get_logger() + +@log_indent +def example_function(): + logger.info('This is a log message that might be quite long and needs proper alignment') + +example_function() +``` + +**Output:** + +``` +2024-08-15 12:34:56 - INFO - Entering function: example_function {example_function} +2024-08-15 12:34:56 - INFO - This is a log message that might be quite long and needs proper alignment {example_function} +2024-08-15 12:34:56 - INFO - Exiting function: example_function {example_function} +``` + +**Explanation:** + +- The function names are aligned at or after the 80th character column. +- If the message is longer than the specified column, the function name moves further to the right, ensuring the message is not truncated. + +### Message Truncation (Optional) + +You can enable message truncation to limit the length of log messages. Set the `truncate_messages` parameter to `True` when initializing `IndentedLogger`. + +```python +# Setup the logger with message truncation enabled +logger_setup = IndentedLogger( + name='my_logger', + level=logging.INFO, + include_func=True, + truncate_messages=True +) +logger = logger_setup.get_logger() + +@log_indent +def example_function(): + logger.info('This is a very long log message that will be truncated to a maximum length') + +example_function() +``` + +**Output:** + +``` +2024-08-15 12:34:56 - INFO - Entering function: example_function {example_function} +2024-08-15 12:34:56 - INFO - This is a very long log message th...{example_function} +2024-08-15 12:34:56 - INFO - Exiting function: example_function {example_function} +``` + +**Notes:** + +- The messages are truncated to a default maximum length (e.g., 50 characters). +- You can adjust the maximum message length by modifying the `max_message_length` variable in the `IndentFormatter` class. + +## Benefits + +- **Enhanced Readability**: Visually represent the hierarchy and depth of operations in your logs. +- **Organized Logs**: Group related log messages, making it easier to understand nested processes. +- **Simplicity**: Minimalistic design adds just what you need without altering core logging functionalities. +- **Customizable Formatting**: Control inclusion of function names, alignment, and message truncation. +- **Easy Integration**: Works with existing logging setups with minimal changes to your configuration. +- **Flexible Indentation**: Supports both automatic and manual indentation for granular control. + +## License + +IndentedLogger is released under the [MIT License](LICENSE). + +--- + +*Note: If you encounter any issues or have suggestions for improvements, feel free to open an issue or submit a pull request on GitHub.* + +--- + +## Additional Details + +### IndentedLogger Class Parameters + +- `name` (str): The name of the logger. +- `level` (int): Logging level (e.g., `logging.INFO`, `logging.DEBUG`). +- `log_file` (str, optional): Path to a log file. If provided, logs will also be written to this file. +- `include_func` (bool, optional): Whether to include function names in log messages. Default is `False`. +- `truncate_messages` (bool, optional): Whether to truncate long messages. Default is `False`. +- `min_func_name_col` (int, optional): The minimum column at which function names should appear. Default is `80`. + +### Example with All Parameters + +```python +logger_setup = IndentedLogger( + name='my_logger', + level=logging.DEBUG, + log_file='app.log', + include_func=True, + truncate_messages=False, + min_func_name_col=80 +) +logger = logger_setup.get_logger() +``` + +### Customizing Indentation and Formatting + +- **Adjust Indentation Width**: Modify the number of spaces used for each indentation level by changing the multiplication factor in the `IndentFormatter` class. +- **Set Date Format**: Pass a `datefmt` parameter when initializing `IndentedLogger` or `IndentFormatter` to customize the timestamp format. + +### Thread Safety + +IndentedLogger uses thread-local storage to manage indentation levels per thread, ensuring that logs from different threads are correctly indented. -- **Indentation Support**: Add indentation to your log messages based on the depth of operations, helping to clarify the structure of complex processes. -- **Custom Formatter**: Includes an `IndentFormatter` that automatically formats messages with the appropriate indentation. -- **Custom Logger Class**: Extends the standard logging `Logger` class to support managing indentation levels easily. -- **Dual Output**: Supports logging to both console and file, ensuring consistent formatting across all outputs. -- **Minimalist Design**: A simple, straightforward approach to enhancing your logging experience without unnecessary overhead. +### Advanced Usage +For advanced use cases, you can extend or modify the `CustomLogger` and `IndentFormatter` classes to suit your specific requirements. +--- +*This updated documentation reflects the latest features and enhancements made to IndentedLogger, providing you with greater control and flexibility over your logging output.* \ No newline at end of file diff --git a/indented_logger/indented_logger.py b/indented_logger/indented_logger.py index 365ab0e..31e0322 100644 --- a/indented_logger/indented_logger.py +++ b/indented_logger/indented_logger.py @@ -1,41 +1,230 @@ import logging +import functools +import threading + +# ------------------------- +# Thread-Local Indentation +# ------------------------- + +_thread_local = threading.local() + + +def get_indent_level(): + return getattr(_thread_local, 'indent_level', 0) + + +def increase_indent(): + _thread_local.indent_level = get_indent_level() + 1 + + +def decrease_indent(): + current = get_indent_level() + _thread_local.indent_level = current - 1 if current > 0 else 0 + + +# ------------------------- +# Logging Decorator +# ------------------------- + +def log_indent(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + logger = get_current_logger() + # Pass the actual function name via 'extra' + extra = {'funcNameOverride': func.__name__} + logger.info(f"Entering function: {func.__name__}", extra=extra) + increase_indent() + try: + return func(*args, **kwargs) + finally: + decrease_indent() + logger.info(f"Exiting function: {func.__name__}", extra=extra) + + return wrapper + + +# ------------------------- +# Logger Retrieval +# ------------------------- + +_current_logger = None + + +def get_current_logger(): + return _current_logger if _current_logger else logging.getLogger(__name__) + + +# ------------------------- +# Custom Formatter +# ------------------------- class IndentFormatter(logging.Formatter): + def __init__(self, include_func=False, truncate_messages=False, min_func_name_col=80, datefmt=None): + self.include_func = include_func + self.truncate_messages = truncate_messages + self.min_func_name_col = min_func_name_col + if include_func: + fmt = '%(asctime)s - %(levelname)-8s - %(message)s%(padding)s{%(funcName)s}' + else: + fmt = '%(asctime)s - %(levelname)-8s - %(message)s' + super().__init__(fmt=fmt, datefmt=datefmt) + def format(self, record): - indent_level = getattr(record, 'lvl', 0) - indent = ' ' * (indent_level * 4) - record.msg = f"{indent}{record.msg}" + # Use 'funcNameOverride' if provided + func_name = getattr(record, 'funcNameOverride', record.funcName) + record.funcName = func_name + + indent_level = get_indent_level() + manual_indent = getattr(record, 'lvl', 0) + total_indent = indent_level + manual_indent + indent = ' ' * (total_indent * 4) + + # Original message with indentation + message = f"{indent}{record.getMessage()}" + + # Handle message truncation if enabled + if self.truncate_messages: + max_message_length = 50 # or any desired length + if len(message) > max_message_length: + message = message[:max_message_length - 3] + '...' + + # Format asctime and levelname + record.asctime = self.formatTime(record, self.datefmt) + levelname = f"{record.levelname:<8}" + + # Build base log line and calculate its length + base_log = f"{record.asctime} - {levelname} - {message}" + + if self.include_func: + base_log_len = len(base_log) + if base_log_len < self.min_func_name_col: + spaces_needed = self.min_func_name_col - base_log_len + record.padding = ' ' * spaces_needed + else: + record.padding = ' ' + else: + record.padding = '' + + # Set the formatted message + record.msg = message + record.args = () return super().format(record) + +# ------------------------- +# Custom Logger +# ------------------------- + class CustomLogger(logging.Logger): - def _log(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1): + def _log_with_indent(self, level, msg, args, exc_info=None, extra=None, stack_info=False, stacklevel=1): if extra is None: extra = {} - if 'lvl' not in extra: - extra['lvl'] = 0 super()._log(level, msg, args, exc_info, extra, stack_info, stacklevel) + def _log_with_lvl(self, level, msg, args, **kwargs): + stacklevel = kwargs.pop('stacklevel', 3) + extra = kwargs.pop('extra', {}) + lvl = kwargs.pop('lvl', 0) + if 'lvl' in extra: + extra['lvl'] += lvl + else: + extra['lvl'] = lvl + # Merge any remaining kwargs into extra + for key in kwargs: + extra[key] = kwargs[key] + self._log_with_indent(level, msg, args, extra=extra, stacklevel=stacklevel) + + def debug(self, msg, *args, **kwargs): + self._log_with_lvl(logging.DEBUG, msg, args, **kwargs) + + def info(self, msg, *args, **kwargs): + self._log_with_lvl(logging.INFO, msg, args, **kwargs) + + def warning(self, msg, *args, **kwargs): + self._log_with_lvl(logging.WARNING, msg, args, **kwargs) + + def error(self, msg, *args, **kwargs): + self._log_with_lvl(logging.ERROR, msg, args, **kwargs) + + def critical(self, msg, *args, **kwargs): + self._log_with_lvl(logging.CRITICAL, msg, args, **kwargs) + + +# ------------------------- +# Indented Logger Setup +# ------------------------- + class IndentedLogger: - def __init__(self, name, level=logging.DEBUG, log_file=None): + def __init__(self, name, level=logging.DEBUG, log_file=None, include_func=False, truncate_messages=False, + min_func_name_col=80): + global _current_logger logging.setLoggerClass(CustomLogger) self.logger = logging.getLogger(name) + _current_logger = self.logger # Store the logger globally for decorator access if not self.logger.handlers: self.logger.setLevel(level) # Console handler console_handler = logging.StreamHandler() - console_formatter = IndentFormatter('%(asctime)s - %(levelname)s - %(message)s') + console_formatter = IndentFormatter(include_func=include_func, truncate_messages=truncate_messages, + min_func_name_col=min_func_name_col) console_handler.setFormatter(console_formatter) self.logger.addHandler(console_handler) # File handler (if a log_file is specified) if log_file: file_handler = logging.FileHandler(log_file) - file_formatter = IndentFormatter('%(asctime)s - %(levelname)s - %(message)s') + file_formatter = IndentFormatter(include_func=include_func, truncate_messages=truncate_messages, + min_func_name_col=min_func_name_col) file_handler.setFormatter(file_formatter) self.logger.addHandler(file_handler) - self.logger.propagate = False + self.logger.propagate = False # Prevent log messages from being propagated to the root logger def get_logger(self): return self.logger + + +# ------------------------- +# Usage Example +# ------------------------- + +if __name__ == "__main__": + # Initialize the IndentedLogger with function names included and min_func_name_col set + indented_logger = IndentedLogger('my_logger', include_func=True, truncate_messages=False, min_func_name_col=100) + logger = indented_logger.get_logger() + + + @log_indent + def start_process(): + logger.info('Starting process') + load_data() + another_function() + + + @log_indent + def load_data(): + logger.info('Loading data', lvl=1) + logger.warning('Data format deprecated') + try: + # Simulate an error + raise ValueError("Invalid data format") + except ValueError as e: + logger.error(f'Failed to load data: {e}', lvl=2) + logger.critical('System crash') + + + @log_indent + def another_function(): + logger.info('Another function started') + nested_function() + + + @log_indent + def nested_function(): + logger.debug('Inside nested function') + logger.info('Nested function processing') + + + # Start the logging process + start_process() diff --git a/setup.py b/setup.py index 8e978a6..49873df 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='indented_logger', # Package name - version='0.1.1', # Version of your package + version='0.1.5', # Version of your package author='Enes Kuzucu', # Your name description='A module to use common logger module with indentation support ', # Short description