pythonLogs 5.0.2__cp313-cp313-macosx_15_0_arm64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pythonLogs/.env.example +32 -0
- pythonLogs/__init__.py +91 -0
- pythonLogs/basic_log.py +68 -0
- pythonLogs/constants.py +56 -0
- pythonLogs/factory.py +446 -0
- pythonLogs/log_utils.py +359 -0
- pythonLogs/memory_utils.py +182 -0
- pythonLogs/settings.py +56 -0
- pythonLogs/size_rotating.py +137 -0
- pythonLogs/thread_safety.py +156 -0
- pythonLogs/timed_rotating.py +126 -0
- pythonlogs-5.0.2.dist-info/METADATA +578 -0
- pythonlogs-5.0.2.dist-info/RECORD +15 -0
- pythonlogs-5.0.2.dist-info/WHEEL +4 -0
- pythonlogs-5.0.2.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import logging.handlers
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from pythonLogs.constants import MB_TO_BYTES
|
|
7
|
+
from pythonLogs.log_utils import (
|
|
8
|
+
check_directory_permissions,
|
|
9
|
+
check_filename_instance,
|
|
10
|
+
cleanup_logger_handlers,
|
|
11
|
+
get_level,
|
|
12
|
+
get_log_path,
|
|
13
|
+
get_logger_and_formatter,
|
|
14
|
+
get_stream_handler,
|
|
15
|
+
gzip_file_with_sufix,
|
|
16
|
+
remove_old_logs,
|
|
17
|
+
write_stderr,
|
|
18
|
+
)
|
|
19
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
20
|
+
from pythonLogs.settings import get_log_settings
|
|
21
|
+
from pythonLogs.thread_safety import auto_thread_safe
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@auto_thread_safe(['init', '_cleanup_logger'])
|
|
25
|
+
class SizeRotatingLog:
|
|
26
|
+
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
|
|
27
|
+
|
|
28
|
+
def __init__(
|
|
29
|
+
self,
|
|
30
|
+
level: Optional[str] = None,
|
|
31
|
+
name: Optional[str] = None,
|
|
32
|
+
directory: Optional[str] = None,
|
|
33
|
+
filenames: Optional[list | tuple] = None,
|
|
34
|
+
maxmbytes: Optional[int] = None,
|
|
35
|
+
daystokeep: Optional[int] = None,
|
|
36
|
+
encoding: Optional[str] = None,
|
|
37
|
+
datefmt: Optional[str] = None,
|
|
38
|
+
timezone: Optional[str] = None,
|
|
39
|
+
streamhandler: Optional[bool] = None,
|
|
40
|
+
showlocation: Optional[bool] = None,
|
|
41
|
+
):
|
|
42
|
+
_settings = get_log_settings()
|
|
43
|
+
self.level = get_level(level or _settings.level)
|
|
44
|
+
self.appname = name or _settings.appname
|
|
45
|
+
self.directory = directory or _settings.directory
|
|
46
|
+
self.filenames = filenames or (_settings.filename,)
|
|
47
|
+
self.maxmbytes = maxmbytes or _settings.max_file_size_mb
|
|
48
|
+
self.daystokeep = daystokeep or _settings.days_to_keep
|
|
49
|
+
self.encoding = encoding or _settings.encoding
|
|
50
|
+
self.datefmt = datefmt or _settings.date_format
|
|
51
|
+
self.timezone = timezone or _settings.timezone
|
|
52
|
+
self.streamhandler = streamhandler or _settings.stream_handler
|
|
53
|
+
self.showlocation = showlocation or _settings.show_location
|
|
54
|
+
self.logger = None
|
|
55
|
+
|
|
56
|
+
def init(self):
|
|
57
|
+
check_filename_instance(self.filenames)
|
|
58
|
+
check_directory_permissions(self.directory)
|
|
59
|
+
|
|
60
|
+
logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
|
|
61
|
+
logger.setLevel(self.level)
|
|
62
|
+
|
|
63
|
+
for file in self.filenames:
|
|
64
|
+
log_file_path = get_log_path(self.directory, file)
|
|
65
|
+
|
|
66
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
67
|
+
filename=log_file_path,
|
|
68
|
+
mode="a",
|
|
69
|
+
maxBytes=self.maxmbytes * MB_TO_BYTES,
|
|
70
|
+
backupCount=self.daystokeep,
|
|
71
|
+
encoding=self.encoding,
|
|
72
|
+
delay=False,
|
|
73
|
+
errors=None,
|
|
74
|
+
)
|
|
75
|
+
file_handler.rotator = GZipRotatorSize(self.directory, self.daystokeep)
|
|
76
|
+
file_handler.setFormatter(formatter)
|
|
77
|
+
file_handler.setLevel(self.level)
|
|
78
|
+
logger.addHandler(file_handler)
|
|
79
|
+
|
|
80
|
+
if self.streamhandler:
|
|
81
|
+
stream_hdlr = get_stream_handler(self.level, formatter)
|
|
82
|
+
logger.addHandler(stream_hdlr)
|
|
83
|
+
|
|
84
|
+
self.logger = logger
|
|
85
|
+
# Register weak reference for memory tracking
|
|
86
|
+
register_logger_weakref(logger)
|
|
87
|
+
return logger
|
|
88
|
+
|
|
89
|
+
def __enter__(self):
|
|
90
|
+
"""Context manager entry."""
|
|
91
|
+
if not hasattr(self, 'logger') or self.logger is None:
|
|
92
|
+
self.init()
|
|
93
|
+
return self.logger
|
|
94
|
+
|
|
95
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
96
|
+
"""Context manager exit with automatic cleanup."""
|
|
97
|
+
if hasattr(self, 'logger'):
|
|
98
|
+
self._cleanup_logger(self.logger)
|
|
99
|
+
|
|
100
|
+
def _cleanup_logger(self, logger: logging.Logger) -> None:
|
|
101
|
+
"""Clean up logger resources by closing all handlers with thread safety."""
|
|
102
|
+
cleanup_logger_handlers(logger)
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def cleanup_logger(logger: logging.Logger) -> None:
|
|
106
|
+
"""Static method for cleaning up logger resources (backward compatibility)."""
|
|
107
|
+
cleanup_logger_handlers(logger)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
class GZipRotatorSize:
|
|
111
|
+
def __init__(self, dir_logs: str, daystokeep: int):
|
|
112
|
+
self.directory = dir_logs
|
|
113
|
+
self.daystokeep = daystokeep
|
|
114
|
+
|
|
115
|
+
def __call__(self, source: str, dest: str) -> None:
|
|
116
|
+
remove_old_logs(self.directory, self.daystokeep)
|
|
117
|
+
if os.path.isfile(source) and os.stat(source).st_size > 0:
|
|
118
|
+
source_filename, _ = os.path.basename(source).split(".")
|
|
119
|
+
new_file_number = self._get_new_file_number(self.directory, source_filename)
|
|
120
|
+
if os.path.isfile(source):
|
|
121
|
+
gzip_file_with_sufix(source, str(new_file_number))
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _get_new_file_number(directory: str, source_filename: str) -> int:
|
|
125
|
+
pattern = re.compile(rf"{re.escape(source_filename)}_(\d+)\.log\.gz$")
|
|
126
|
+
max_num = 0
|
|
127
|
+
try:
|
|
128
|
+
# Use pathlib for better performance with large directories
|
|
129
|
+
dir_path = Path(directory)
|
|
130
|
+
for file_path in dir_path.iterdir():
|
|
131
|
+
if file_path.is_file():
|
|
132
|
+
match = pattern.match(file_path.name)
|
|
133
|
+
if match:
|
|
134
|
+
max_num = max(max_num, int(match.group(1)))
|
|
135
|
+
except OSError as e:
|
|
136
|
+
write_stderr(f"Unable to get previous gz log file number | {repr(e)}")
|
|
137
|
+
return max_num + 1
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import functools
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Any, Callable, Dict, Type, TypeVar
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ThreadSafeMeta(type):
|
|
10
|
+
"""Metaclass that automatically adds thread safety to class methods."""
|
|
11
|
+
|
|
12
|
+
def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
|
|
13
|
+
# Create the class first
|
|
14
|
+
cls = super().__new__(mcs, name, bases, namespace)
|
|
15
|
+
|
|
16
|
+
# Add a class-level lock if not already present
|
|
17
|
+
if not hasattr(cls, '_lock'):
|
|
18
|
+
cls._lock = threading.RLock()
|
|
19
|
+
|
|
20
|
+
# Get methods that should be thread-safe (exclude private/dunder methods)
|
|
21
|
+
thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
|
|
22
|
+
if thread_safe_methods is None:
|
|
23
|
+
# Auto-detect public methods that modify state
|
|
24
|
+
thread_safe_methods = [
|
|
25
|
+
method_name
|
|
26
|
+
for method_name in namespace
|
|
27
|
+
if (
|
|
28
|
+
callable(getattr(cls, method_name, None))
|
|
29
|
+
and not method_name.startswith('_')
|
|
30
|
+
and method_name not in ['__enter__', '__exit__', '__init__']
|
|
31
|
+
)
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
# Wrap each method with automatic locking
|
|
35
|
+
for method_name in thread_safe_methods:
|
|
36
|
+
if hasattr(cls, method_name):
|
|
37
|
+
original_method = getattr(cls, method_name)
|
|
38
|
+
if callable(original_method):
|
|
39
|
+
wrapped_method = thread_safe(original_method)
|
|
40
|
+
setattr(cls, method_name, wrapped_method)
|
|
41
|
+
|
|
42
|
+
return cls
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def thread_safe(func: F) -> F:
|
|
46
|
+
"""Decorator that automatically adds thread safety to methods."""
|
|
47
|
+
|
|
48
|
+
@functools.wraps(func)
|
|
49
|
+
def wrapper(self, *args, **kwargs):
|
|
50
|
+
# Use instance lock if available, otherwise class lock
|
|
51
|
+
lock = getattr(self, '_lock', None)
|
|
52
|
+
if lock is None:
|
|
53
|
+
# Check if class has lock, if not create one
|
|
54
|
+
if not hasattr(self.__class__, '_lock'):
|
|
55
|
+
self.__class__._lock = threading.RLock()
|
|
56
|
+
lock = self.__class__._lock
|
|
57
|
+
|
|
58
|
+
with lock:
|
|
59
|
+
return func(self, *args, **kwargs)
|
|
60
|
+
|
|
61
|
+
return wrapper
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _get_wrappable_methods(cls: Type) -> list:
|
|
65
|
+
"""Helper function to get methods that should be made thread-safe."""
|
|
66
|
+
return [
|
|
67
|
+
method_name
|
|
68
|
+
for method_name in dir(cls)
|
|
69
|
+
if (
|
|
70
|
+
callable(getattr(cls, method_name, None))
|
|
71
|
+
and not method_name.startswith('_')
|
|
72
|
+
and method_name not in ['__enter__', '__exit__', '__init__']
|
|
73
|
+
)
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _ensure_class_has_lock(cls: Type) -> None:
|
|
78
|
+
"""Ensure the class has a lock attribute."""
|
|
79
|
+
if not hasattr(cls, '_lock'):
|
|
80
|
+
cls._lock = threading.RLock()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
|
|
84
|
+
"""Check if a method should be wrapped with thread safety."""
|
|
85
|
+
return (
|
|
86
|
+
hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped')
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def auto_thread_safe(thread_safe_methods: list = None):
|
|
91
|
+
"""Class decorator that adds automatic thread safety to specified methods."""
|
|
92
|
+
|
|
93
|
+
def decorator(cls: Type) -> Type:
|
|
94
|
+
_ensure_class_has_lock(cls)
|
|
95
|
+
|
|
96
|
+
# Store thread-safe methods list
|
|
97
|
+
if thread_safe_methods:
|
|
98
|
+
cls._thread_safe_methods = thread_safe_methods
|
|
99
|
+
|
|
100
|
+
# Get methods to make thread-safe
|
|
101
|
+
methods_to_wrap = thread_safe_methods or _get_wrappable_methods(cls)
|
|
102
|
+
|
|
103
|
+
# Wrap each method
|
|
104
|
+
for method_name in methods_to_wrap:
|
|
105
|
+
original_method = getattr(cls, method_name, None)
|
|
106
|
+
if _should_wrap_method(cls, method_name, original_method):
|
|
107
|
+
wrapped_method = thread_safe(original_method)
|
|
108
|
+
wrapped_method._thread_safe_wrapped = True
|
|
109
|
+
setattr(cls, method_name, wrapped_method)
|
|
110
|
+
|
|
111
|
+
return cls
|
|
112
|
+
|
|
113
|
+
return decorator
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class AutoThreadSafe:
|
|
117
|
+
"""Base class that provides automatic thread safety for all public methods."""
|
|
118
|
+
|
|
119
|
+
def __init__(self):
|
|
120
|
+
if not hasattr(self, '_lock'):
|
|
121
|
+
self._lock = threading.RLock()
|
|
122
|
+
|
|
123
|
+
def __init_subclass__(cls, **kwargs):
|
|
124
|
+
super().__init_subclass__(**kwargs)
|
|
125
|
+
|
|
126
|
+
# Add class-level lock
|
|
127
|
+
if not hasattr(cls, '_lock'):
|
|
128
|
+
cls._lock = threading.RLock()
|
|
129
|
+
|
|
130
|
+
# Auto-wrap public methods
|
|
131
|
+
for attr_name in dir(cls):
|
|
132
|
+
if not attr_name.startswith('_'):
|
|
133
|
+
attr = getattr(cls, attr_name)
|
|
134
|
+
if callable(attr) and not hasattr(attr, '_thread_safe_wrapped'):
|
|
135
|
+
wrapped_attr = thread_safe(attr)
|
|
136
|
+
wrapped_attr._thread_safe_wrapped = True
|
|
137
|
+
setattr(cls, attr_name, wrapped_attr)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def synchronized_method(func: F) -> F:
|
|
141
|
+
"""Decorator for individual methods that need thread safety."""
|
|
142
|
+
return thread_safe(func)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
class ThreadSafeContext:
|
|
146
|
+
"""Context manager for thread-safe operations."""
|
|
147
|
+
|
|
148
|
+
def __init__(self, lock: threading.Lock):
|
|
149
|
+
self.lock = lock
|
|
150
|
+
|
|
151
|
+
def __enter__(self):
|
|
152
|
+
self.lock.acquire()
|
|
153
|
+
return self
|
|
154
|
+
|
|
155
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
156
|
+
self.lock.release()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import logging.handlers
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pythonLogs.log_utils import (
|
|
5
|
+
check_directory_permissions,
|
|
6
|
+
check_filename_instance,
|
|
7
|
+
cleanup_logger_handlers,
|
|
8
|
+
get_level,
|
|
9
|
+
get_log_path,
|
|
10
|
+
get_logger_and_formatter,
|
|
11
|
+
get_stream_handler,
|
|
12
|
+
gzip_file_with_sufix,
|
|
13
|
+
remove_old_logs,
|
|
14
|
+
)
|
|
15
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
16
|
+
from pythonLogs.settings import get_log_settings
|
|
17
|
+
from pythonLogs.thread_safety import auto_thread_safe
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@auto_thread_safe(['init', '_cleanup_logger'])
|
|
21
|
+
class TimedRotatingLog:
|
|
22
|
+
"""
|
|
23
|
+
Time-based rotating logger with context manager support for automatic resource cleanup.
|
|
24
|
+
|
|
25
|
+
Current 'rotating_when' events supported for TimedRotatingLogs:
|
|
26
|
+
Use RotateWhen enum values:
|
|
27
|
+
RotateWhen.MIDNIGHT - roll over at midnight
|
|
28
|
+
RotateWhen.MONDAY through RotateWhen.SUNDAY - roll over on specific days
|
|
29
|
+
RotateWhen.HOURLY - roll over every hour
|
|
30
|
+
RotateWhen.DAILY - roll over daily
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
level: Optional[str] = None,
|
|
36
|
+
name: Optional[str] = None,
|
|
37
|
+
directory: Optional[str] = None,
|
|
38
|
+
filenames: Optional[list | tuple] = None,
|
|
39
|
+
when: Optional[str] = None,
|
|
40
|
+
sufix: Optional[str] = None,
|
|
41
|
+
daystokeep: Optional[int] = None,
|
|
42
|
+
encoding: Optional[str] = None,
|
|
43
|
+
datefmt: Optional[str] = None,
|
|
44
|
+
timezone: Optional[str] = None,
|
|
45
|
+
streamhandler: Optional[bool] = None,
|
|
46
|
+
showlocation: Optional[bool] = None,
|
|
47
|
+
rotateatutc: Optional[bool] = None,
|
|
48
|
+
):
|
|
49
|
+
_settings = get_log_settings()
|
|
50
|
+
self.level = get_level(level or _settings.level)
|
|
51
|
+
self.appname = name or _settings.appname
|
|
52
|
+
self.directory = directory or _settings.directory
|
|
53
|
+
self.filenames = filenames or (_settings.filename,)
|
|
54
|
+
self.when = when or _settings.rotate_when
|
|
55
|
+
self.sufix = sufix or _settings.rotate_file_sufix
|
|
56
|
+
self.daystokeep = daystokeep or _settings.days_to_keep
|
|
57
|
+
self.encoding = encoding or _settings.encoding
|
|
58
|
+
self.datefmt = datefmt or _settings.date_format
|
|
59
|
+
self.timezone = timezone or _settings.timezone
|
|
60
|
+
self.streamhandler = streamhandler or _settings.stream_handler
|
|
61
|
+
self.showlocation = showlocation or _settings.show_location
|
|
62
|
+
self.rotateatutc = rotateatutc or _settings.rotate_at_utc
|
|
63
|
+
self.logger = None
|
|
64
|
+
|
|
65
|
+
def init(self):
|
|
66
|
+
check_filename_instance(self.filenames)
|
|
67
|
+
check_directory_permissions(self.directory)
|
|
68
|
+
|
|
69
|
+
logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
|
|
70
|
+
logger.setLevel(self.level)
|
|
71
|
+
|
|
72
|
+
for file in self.filenames:
|
|
73
|
+
log_file_path = get_log_path(self.directory, file)
|
|
74
|
+
|
|
75
|
+
file_handler = logging.handlers.TimedRotatingFileHandler(
|
|
76
|
+
filename=log_file_path,
|
|
77
|
+
encoding=self.encoding,
|
|
78
|
+
when=self.when,
|
|
79
|
+
utc=self.rotateatutc,
|
|
80
|
+
backupCount=self.daystokeep,
|
|
81
|
+
)
|
|
82
|
+
file_handler.suffix = self.sufix
|
|
83
|
+
file_handler.rotator = GZipRotatorTimed(self.directory, self.daystokeep)
|
|
84
|
+
file_handler.setFormatter(formatter)
|
|
85
|
+
file_handler.setLevel(self.level)
|
|
86
|
+
logger.addHandler(file_handler)
|
|
87
|
+
|
|
88
|
+
if self.streamhandler:
|
|
89
|
+
stream_hdlr = get_stream_handler(self.level, formatter)
|
|
90
|
+
logger.addHandler(stream_hdlr)
|
|
91
|
+
|
|
92
|
+
self.logger = logger
|
|
93
|
+
# Register weak reference for memory tracking
|
|
94
|
+
register_logger_weakref(logger)
|
|
95
|
+
return logger
|
|
96
|
+
|
|
97
|
+
def __enter__(self):
|
|
98
|
+
"""Context manager entry."""
|
|
99
|
+
if not hasattr(self, 'logger') or self.logger is None:
|
|
100
|
+
self.init()
|
|
101
|
+
return self.logger
|
|
102
|
+
|
|
103
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
104
|
+
"""Context manager exit with automatic cleanup."""
|
|
105
|
+
if hasattr(self, 'logger'):
|
|
106
|
+
self._cleanup_logger(self.logger)
|
|
107
|
+
|
|
108
|
+
def _cleanup_logger(self, logger: logging.Logger) -> None:
|
|
109
|
+
"""Clean up logger resources by closing all handlers with thread safety."""
|
|
110
|
+
cleanup_logger_handlers(logger)
|
|
111
|
+
|
|
112
|
+
@staticmethod
|
|
113
|
+
def cleanup_logger(logger: logging.Logger) -> None:
|
|
114
|
+
"""Static method for cleaning up logger resources (backward compatibility)."""
|
|
115
|
+
cleanup_logger_handlers(logger)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class GZipRotatorTimed:
|
|
119
|
+
def __init__(self, dir_logs: str, days_to_keep: int):
|
|
120
|
+
self.dir = dir_logs
|
|
121
|
+
self.days_to_keep = days_to_keep
|
|
122
|
+
|
|
123
|
+
def __call__(self, source: str, dest: str) -> None:
|
|
124
|
+
remove_old_logs(self.dir, self.days_to_keep)
|
|
125
|
+
sufix = os.path.splitext(dest)[1].replace(".", "")
|
|
126
|
+
gzip_file_with_sufix(source, sufix)
|