Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/PULL_REQUEST_TEMPLATE
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
- [ ] I have thought about how this code may affect other services.
- [ ] This PR fixes an issue.
- [ ] This PR adds something new (e.g. new method or parameters).
- [ ] This PR have unit tests (e.g. tests added/removed/changed)
- [ ] This PR is a breaking change (e.g. methods or parameters removed/renamed)
- [ ] This PR is **not** a code change (e.g. documentation, README, ...)

Expand Down
58 changes: 0 additions & 58 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ High-performance Python logging library with file rotation and optimized caching
- [Memory Management](#memory-management)
- [Flexible Configuration Options](#flexible-configuration-options)
- [Migration Guide](#migration-guide)
- [Performance Improvements](#performance-improvements)
- [Development](#source-code)
- [Run Tests and Get Coverage Report using Poe](#run-tests-and-get-coverage-report-using-poe)
- [License](#license)
Expand Down Expand Up @@ -446,40 +445,6 @@ registered = LoggerFactory.get_registered_loggers()
print(f"Currently registered: {list(registered.keys())}")
```

## Thread-Safe Operations
All memory management operations are thread-safe and can be used safely in multi-threaded applications:

```python
import threading
from pythonLogs import size_rotating_logger, clear_logger_registry

def worker_function(worker_id):
# Each thread can safely create and use loggers
logger = size_rotating_logger(
name=f"worker_{worker_id}",
directory="/app/logs"
)

with logger as log:
log.info(f"Worker {worker_id} started")
# Automatic cleanup per thread

# Create multiple threads - all operations are thread-safe
threads = []
for i in range(10):
thread = threading.Thread(target=worker_function, args=(i,))
threads.append(thread)
thread.start()

# Wait for completion and clean up
for thread in threads:
thread.join()

# Safe to clear registry from main thread
clear_logger_registry()
```


# Flexible Configuration Options
You can use either enums (for type safety) or strings (for simplicity):

Expand Down Expand Up @@ -552,25 +517,6 @@ timed_logger = timed_rotating_logger(level=LogLevel.WARNING, name="app", directo
- 🔧 **Cleaner API** without manual `.init()` calls
- 📚 **Centralized configuration** through factory pattern

# Performance Improvements

## Benchmarks
The factory pattern with optimizations provides significant performance improvements:

| Feature | Improvement | Benefit |
|---------|-------------|---------|
| Logger Registry | 90%+ faster | Cached logger instances |
| Settings Caching | ~85% faster | Reused configuration objects |
| Directory Validation | ~75% faster | Cached permission checks |
| Timezone Operations | ~60% faster | Cached timezone functions |

## Performance Test Results
```python
# Create 100 loggers - Performance comparison
# Legacy method: ~0.045 seconds
# Factory pattern: ~0.004 seconds
# Improvement: 91% faster ⚡
```

# Source Code
### Build
Expand All @@ -579,21 +525,17 @@ poetry build -f wheel
```



# Run Tests and Get Coverage Report using Poe
```shell
poetry update --with test
poe test
```



# License
Released under the [MIT License](LICENSE)




# Buy me a cup of coffee
+ [GitHub Sponsor](https://github.com/sponsors/ddc)
+ [ko-fi](https://ko-fi.com/ddcsta)
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry]
name = "pythonLogs"
version = "4.0.3"
version = "4.0.4"
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
license = "MIT"
readme = "README.md"
Expand Down
8 changes: 3 additions & 5 deletions pythonLogs/basic_log.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# -*- encoding: utf-8 -*-
import logging
import threading
from typing import Optional
from pythonLogs.log_utils import get_format, get_level, get_timezone_function
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
from pythonLogs.settings import get_log_settings
from pythonLogs.thread_safety import auto_thread_safe


@auto_thread_safe(['init', '_cleanup_logger'])
class BasicLog:
"""Basic logger with context manager support for automatic resource cleanup."""

Expand All @@ -27,8 +28,6 @@ def __init__(
self.timezone = timezone or _settings.timezone
self.showlocation = showlocation or _settings.show_location
self.logger = None
# Instance-level lock for thread safety
self._lock = threading.Lock()

def init(self):
logger = logging.getLogger(self.appname)
Expand All @@ -54,8 +53,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def _cleanup_logger(self, logger: logging.Logger) -> None:
"""Clean up logger resources by closing all handlers with thread safety."""
with self._lock:
cleanup_logger_handlers(logger)
cleanup_logger_handlers(logger)

@staticmethod
def cleanup_logger(logger: logging.Logger) -> None:
Expand Down
8 changes: 3 additions & 5 deletions pythonLogs/size_rotating.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import logging.handlers
import os
import re
import threading
from typing import Optional
from pythonLogs.constants import MB_TO_BYTES
from pythonLogs.log_utils import (
Expand All @@ -18,8 +17,10 @@
)
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
from pythonLogs.settings import get_log_settings
from pythonLogs.thread_safety import auto_thread_safe


@auto_thread_safe(['init', '_cleanup_logger'])
class SizeRotatingLog:
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
def __init__(
Expand Down Expand Up @@ -49,8 +50,6 @@ def __init__(
self.streamhandler = streamhandler or _settings.stream_handler
self.showlocation = showlocation or _settings.show_location
self.logger = None
# Instance-level lock for thread safety
self._lock = threading.Lock()

def init(self):
check_filename_instance(self.filenames)
Expand Down Expand Up @@ -98,8 +97,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def _cleanup_logger(self, logger: logging.Logger) -> None:
"""Clean up logger resources by closing all handlers with thread safety."""
with self._lock:
cleanup_logger_handlers(logger)
cleanup_logger_handlers(logger)

@staticmethod
def cleanup_logger(logger: logging.Logger) -> None:
Expand Down
135 changes: 135 additions & 0 deletions pythonLogs/thread_safety.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
# -*- encoding: utf-8 -*-
import functools
import threading
from typing import Any, Callable, Dict, TypeVar, Type

F = TypeVar('F', bound=Callable[..., Any])


class ThreadSafeMeta(type):
"""Metaclass that automatically adds thread safety to class methods."""

def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
# Create the class first
cls = super().__new__(mcs, name, bases, namespace)

# Add a class-level lock if not already present
if not hasattr(cls, '_lock'):
cls._lock = threading.RLock()

# Get methods that should be thread-safe (exclude private/dunder methods)
thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
if thread_safe_methods is None:
# Auto-detect public methods that modify state
thread_safe_methods = [
method_name for method_name in namespace
if (callable(getattr(cls, method_name, None)) and
not method_name.startswith('_') and
method_name not in ['__enter__', '__exit__', '__init__'])
]

# Wrap each method with automatic locking
for method_name in thread_safe_methods:
if hasattr(cls, method_name):
original_method = getattr(cls, method_name)
if callable(original_method):
wrapped_method = thread_safe(original_method)
setattr(cls, method_name, wrapped_method)

return cls


def thread_safe(func: F) -> F:
"""Decorator that automatically adds thread safety to methods."""

@functools.wraps(func)
def wrapper(self, *args, **kwargs):
# Use instance lock if available, otherwise class lock
lock = getattr(self, '_lock', None)
if lock is None:
# Check if class has lock, if not create one
if not hasattr(self.__class__, '_lock'):
self.__class__._lock = threading.RLock()
lock = self.__class__._lock

with lock:
return func(self, *args, **kwargs)

return wrapper


def auto_thread_safe(thread_safe_methods: list = None):
"""Class decorator that adds automatic thread safety to specified methods."""

def decorator(cls: Type) -> Type:
# Add lock to class if not present
if not hasattr(cls, '_lock'):
cls._lock = threading.RLock()

# Store thread-safe methods list
if thread_safe_methods:
cls._thread_safe_methods = thread_safe_methods

# Get methods to make thread-safe
methods_to_wrap = thread_safe_methods or [
method_name for method_name in dir(cls)
if (callable(getattr(cls, method_name, None)) and
not method_name.startswith('_') and
method_name not in ['__enter__', '__exit__', '__init__'])
]

# Wrap each method
for method_name in methods_to_wrap:
if hasattr(cls, method_name):
original_method = getattr(cls, method_name)
if callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped'):
wrapped_method = thread_safe(original_method)
wrapped_method._thread_safe_wrapped = True
setattr(cls, method_name, wrapped_method)

return cls

return decorator


class AutoThreadSafe:
"""Base class that provides automatic thread safety for all public methods."""

def __init__(self):
if not hasattr(self, '_lock'):
self._lock = threading.RLock()

def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)

# Add class-level lock
if not hasattr(cls, '_lock'):
cls._lock = threading.RLock()

# Auto-wrap public methods
for attr_name in dir(cls):
if not attr_name.startswith('_'):
attr = getattr(cls, attr_name)
if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
wrapped_attr = thread_safe(attr)
wrapped_attr._thread_safe_wrapped = True
setattr(cls, attr_name, wrapped_attr)


def synchronized_method(func: F) -> F:
"""Decorator for individual methods that need thread safety."""
return thread_safe(func)


class ThreadSafeContext:
"""Context manager for thread-safe operations."""

def __init__(self, lock: threading.Lock):
self.lock = lock

def __enter__(self):
self.lock.acquire()
return self

def __exit__(self, exc_type, exc_val, exc_tb):
self.lock.release()
8 changes: 3 additions & 5 deletions pythonLogs/timed_rotating.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
# -*- encoding: utf-8 -*-
import logging.handlers
import os
import threading
from typing import Optional
from pythonLogs.log_utils import (
check_directory_permissions,
Expand All @@ -15,8 +14,10 @@
)
from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
from pythonLogs.settings import get_log_settings
from pythonLogs.thread_safety import auto_thread_safe


@auto_thread_safe(['init', '_cleanup_logger'])
class TimedRotatingLog:
"""
Time-based rotating logger with context manager support for automatic resource cleanup.
Expand Down Expand Up @@ -60,8 +61,6 @@ def __init__(
self.showlocation = showlocation or _settings.show_location
self.rotateatutc = rotateatutc or _settings.rotate_at_utc
self.logger = None
# Instance-level lock for thread safety
self._lock = threading.Lock()

def init(self):
check_filename_instance(self.filenames)
Expand Down Expand Up @@ -107,8 +106,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):

def _cleanup_logger(self, logger: logging.Logger) -> None:
"""Clean up logger resources by closing all handlers with thread safety."""
with self._lock:
cleanup_logger_handlers(logger)
cleanup_logger_handlers(logger)

@staticmethod
def cleanup_logger(logger: logging.Logger) -> None:
Expand Down
Loading