pythonLogs 5.0.0__tar.gz → 5.0.2__tar.gz
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-5.0.0 → pythonlogs-5.0.2}/PKG-INFO +5 -3
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pyproject.toml +31 -20
- pythonlogs-5.0.2/pythonLogs/.env.example +32 -0
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/__init__.py +4 -9
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/basic_log.py +4 -4
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/constants.py +2 -1
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/factory.py +47 -33
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/log_utils.py +115 -22
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/memory_utils.py +34 -60
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/settings.py +1 -2
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/size_rotating.py +8 -7
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/thread_safety.py +38 -33
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/pythonLogs/timed_rotating.py +11 -10
- pythonlogs-5.0.0/pythonLogs/.env.example +0 -20
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/LICENSE +0 -0
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/README.md +0 -0
- {pythonlogs-5.0.0 → pythonlogs-5.0.2}/build.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonLogs
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.2
|
|
4
4
|
Summary: High-performance Python logging library with file rotation and optimized caching for better performance
|
|
5
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
6
7
|
Keywords: python3,python-3,python,log,logging,logger,logutils,log-utils,pythonLogs
|
|
7
8
|
Author: Daniel Costa
|
|
8
9
|
Author-email: danieldcsta@gmail.com
|
|
@@ -17,9 +18,10 @@ Classifier: Operating System :: OS Independent
|
|
|
17
18
|
Classifier: Programming Language :: Python :: 3
|
|
18
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
22
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
21
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
22
|
-
Requires-Dist: pydantic-settings (
|
|
24
|
+
Requires-Dist: pydantic-settings (==2.11.0)
|
|
23
25
|
Requires-Dist: python-dotenv (>=1.1.1,<2.0.0)
|
|
24
26
|
Project-URL: Homepage, https://pypi.org/project/pythonLogs
|
|
25
27
|
Project-URL: Repository, https://github.com/ddc/pythonLogs
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["poetry-core
|
|
2
|
+
requires = ["poetry-core"]
|
|
3
3
|
build-backend = "poetry.core.masonry.api"
|
|
4
4
|
|
|
5
5
|
[tool.poetry]
|
|
6
6
|
name = "pythonLogs"
|
|
7
|
-
version = "5.0.
|
|
7
|
+
version = "5.0.2"
|
|
8
8
|
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
readme = "README.md"
|
|
@@ -39,37 +39,48 @@ optional = true
|
|
|
39
39
|
|
|
40
40
|
[tool.poetry.dependencies]
|
|
41
41
|
python = "^3.12"
|
|
42
|
-
pydantic-settings = "
|
|
42
|
+
pydantic-settings = "2.11.0"
|
|
43
43
|
python-dotenv = "^1.1.1"
|
|
44
44
|
|
|
45
45
|
[tool.poetry.group.test.dependencies]
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
pytest = "^
|
|
46
|
+
poethepoet = "^0.37.0"
|
|
47
|
+
psutil = "^7.1.0"
|
|
48
|
+
pytest = "^8.4.2"
|
|
49
|
+
pytest-cov = "^7.0.0"
|
|
50
50
|
|
|
51
51
|
[tool.poe.tasks]
|
|
52
|
-
_test = "
|
|
53
|
-
|
|
54
|
-
_coverage_xml = "coverage xml"
|
|
55
|
-
tests = ["_test", "_coverage_report", "_coverage_xml"]
|
|
52
|
+
_test = "python -m pytest -v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml -o junit_family=legacy"
|
|
53
|
+
tests = ["_test"]
|
|
56
54
|
test = ["tests"]
|
|
57
55
|
|
|
58
|
-
[tool.black]
|
|
59
|
-
line-length = 120
|
|
60
|
-
skip-string-normalization = true
|
|
61
|
-
|
|
62
|
-
[tool.pytest.ini_options]
|
|
63
|
-
markers = [
|
|
64
|
-
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
|
|
65
|
-
]
|
|
66
|
-
|
|
67
56
|
[tool.coverage.run]
|
|
68
57
|
omit = [
|
|
58
|
+
"build.py",
|
|
69
59
|
"tests/*",
|
|
60
|
+
"*/__init__.py",
|
|
70
61
|
]
|
|
71
62
|
|
|
72
63
|
[tool.coverage.report]
|
|
73
64
|
exclude_lines = [
|
|
74
65
|
"pragma: no cover",
|
|
66
|
+
"def __repr__",
|
|
67
|
+
"if self.debug:",
|
|
68
|
+
"if settings.DEBUG",
|
|
69
|
+
"raise AssertionError",
|
|
70
|
+
"raise NotImplementedError",
|
|
71
|
+
"if 0:",
|
|
72
|
+
"if __name__ == .__main__.:",
|
|
73
|
+
"class .*\\bProtocol\\):",
|
|
74
|
+
"@(abc\\.)?abstractmethod",
|
|
75
75
|
]
|
|
76
|
+
show_missing = false
|
|
77
|
+
skip_covered = false
|
|
78
|
+
|
|
79
|
+
[tool.pytest.ini_options]
|
|
80
|
+
markers = [
|
|
81
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.black]
|
|
85
|
+
line-length = 120
|
|
86
|
+
skip-string-normalization = true
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# pythonLogs Environment Configuration
|
|
2
|
+
# Copy this file to .env and modify values as needed
|
|
3
|
+
|
|
4
|
+
# Basic Logger Settings
|
|
5
|
+
LOG_LEVEL=DEBUG
|
|
6
|
+
LOG_TIMEZONE=UTC
|
|
7
|
+
LOG_ENCODING=UTF-8
|
|
8
|
+
LOG_APPNAME=app
|
|
9
|
+
LOG_FILENAME=app.log
|
|
10
|
+
LOG_DIRECTORY=/app/logs
|
|
11
|
+
LOG_DAYS_TO_KEEP=30
|
|
12
|
+
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
|
|
13
|
+
LOG_STREAM_HANDLER=True
|
|
14
|
+
LOG_SHOW_LOCATION=False
|
|
15
|
+
|
|
16
|
+
# Memory Management Settings
|
|
17
|
+
LOG_MAX_LOGGERS=50
|
|
18
|
+
LOG_LOGGER_TTL_SECONDS=1800
|
|
19
|
+
|
|
20
|
+
# SizeRotatingLog Settings
|
|
21
|
+
LOG_MAX_FILE_SIZE_MB=10
|
|
22
|
+
|
|
23
|
+
# TimedRotatingLog Settings
|
|
24
|
+
LOG_ROTATE_WHEN=midnight
|
|
25
|
+
LOG_ROTATE_AT_UTC=True
|
|
26
|
+
LOG_ROTATE_FILE_SUFIX=%Y%m%d
|
|
27
|
+
|
|
28
|
+
# Available LOG_LEVEL values: DEBUG, INFO, WARNING, ERROR, CRITICAL
|
|
29
|
+
# Available LOG_TIMEZONE values: UTC, localtime, or any valid timezone (e.g., America/New_York)
|
|
30
|
+
# Available LOG_ROTATE_WHEN values: midnight, S, M, H, D, W0-W6, daily, hourly, weekly
|
|
31
|
+
# LOG_STREAM_HANDLER: Set to True to enable console output, False to disable
|
|
32
|
+
# LOG_SHOW_LOCATION: Set to True to include filename:function:line in log messages
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
from importlib.metadata import version
|
|
4
3
|
from typing import Literal, NamedTuple
|
|
@@ -14,7 +13,7 @@ from pythonLogs.factory import (
|
|
|
14
13
|
LoggerType,
|
|
15
14
|
shutdown_logger,
|
|
16
15
|
size_rotating_logger,
|
|
17
|
-
timed_rotating_logger
|
|
16
|
+
timed_rotating_logger,
|
|
18
17
|
)
|
|
19
18
|
from pythonLogs.memory_utils import (
|
|
20
19
|
clear_directory_cache,
|
|
@@ -22,7 +21,7 @@ from pythonLogs.memory_utils import (
|
|
|
22
21
|
force_garbage_collection,
|
|
23
22
|
get_memory_stats,
|
|
24
23
|
optimize_lru_cache_sizes,
|
|
25
|
-
set_directory_cache_limit
|
|
24
|
+
set_directory_cache_limit,
|
|
26
25
|
)
|
|
27
26
|
from pythonLogs.size_rotating import SizeRotatingLog
|
|
28
27
|
from pythonLogs.timed_rotating import TimedRotatingLog
|
|
@@ -77,18 +76,14 @@ class VersionInfo(NamedTuple):
|
|
|
77
76
|
|
|
78
77
|
__version__ = _version
|
|
79
78
|
__version_info__: VersionInfo = VersionInfo(
|
|
80
|
-
major=__version__[0],
|
|
81
|
-
minor=__version__[1],
|
|
82
|
-
micro=__version__[2],
|
|
83
|
-
releaselevel="final",
|
|
84
|
-
serial=0
|
|
79
|
+
major=__version__[0], minor=__version__[1], micro=__version__[2], releaselevel="final", serial=0
|
|
85
80
|
)
|
|
86
81
|
__req_python_version__: VersionInfo = VersionInfo(
|
|
87
82
|
major=_req_python_version[0],
|
|
88
83
|
minor=_req_python_version[1],
|
|
89
84
|
micro=_req_python_version[2],
|
|
90
85
|
releaselevel="final",
|
|
91
|
-
serial=0
|
|
86
|
+
serial=0,
|
|
92
87
|
)
|
|
93
88
|
|
|
94
89
|
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
from typing import Optional
|
|
4
3
|
from pythonLogs.log_utils import get_format, get_level, get_timezone_function
|
|
5
|
-
from pythonLogs.
|
|
4
|
+
from pythonLogs.log_utils import cleanup_logger_handlers
|
|
5
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
6
6
|
from pythonLogs.settings import get_log_settings
|
|
7
7
|
from pythonLogs.thread_safety import auto_thread_safe
|
|
8
8
|
|
|
@@ -34,14 +34,14 @@ class BasicLog:
|
|
|
34
34
|
logger.setLevel(self.level)
|
|
35
35
|
logging.Formatter.converter = get_timezone_function(self.timezone)
|
|
36
36
|
_format = get_format(self.showlocation, self.appname, self.timezone)
|
|
37
|
-
|
|
37
|
+
|
|
38
38
|
# Only add handler if logger doesn't have any handlers
|
|
39
39
|
if not logger.handlers:
|
|
40
40
|
handler = logging.StreamHandler()
|
|
41
41
|
formatter = logging.Formatter(_format, datefmt=self.datefmt)
|
|
42
42
|
handler.setFormatter(formatter)
|
|
43
43
|
logger.addHandler(handler)
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
self.logger = logger
|
|
46
46
|
# Register weak reference for memory tracking
|
|
47
47
|
register_logger_weakref(logger)
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
from enum import Enum
|
|
4
3
|
|
|
@@ -20,6 +19,7 @@ DEFAULT_TIMEZONE = "UTC"
|
|
|
20
19
|
|
|
21
20
|
class LogLevel(str, Enum):
|
|
22
21
|
"""Log levels"""
|
|
22
|
+
|
|
23
23
|
CRITICAL = "CRITICAL"
|
|
24
24
|
CRIT = "CRIT"
|
|
25
25
|
ERROR = "ERROR"
|
|
@@ -31,6 +31,7 @@ class LogLevel(str, Enum):
|
|
|
31
31
|
|
|
32
32
|
class RotateWhen(str, Enum):
|
|
33
33
|
"""Rotation timing options for TimedRotatingLog"""
|
|
34
|
+
|
|
34
35
|
MIDNIGHT = "midnight"
|
|
35
36
|
MONDAY = "W0"
|
|
36
37
|
TUESDAY = "W1"
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import atexit
|
|
3
2
|
import logging
|
|
4
3
|
import threading
|
|
@@ -8,7 +7,8 @@ from enum import Enum
|
|
|
8
7
|
from typing import Dict, Optional, Tuple, Union
|
|
9
8
|
from pythonLogs.basic_log import BasicLog
|
|
10
9
|
from pythonLogs.constants import LogLevel, RotateWhen
|
|
11
|
-
from pythonLogs.
|
|
10
|
+
from pythonLogs.log_utils import cleanup_logger_handlers
|
|
11
|
+
from pythonLogs.settings import get_log_settings
|
|
12
12
|
from pythonLogs.size_rotating import SizeRotatingLog
|
|
13
13
|
from pythonLogs.timed_rotating import TimedRotatingLog
|
|
14
14
|
|
|
@@ -16,6 +16,7 @@ from pythonLogs.timed_rotating import TimedRotatingLog
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class LoggerConfig:
|
|
18
18
|
"""Configuration class to group logger parameters"""
|
|
19
|
+
|
|
19
20
|
level: Optional[Union[LogLevel, str]] = None
|
|
20
21
|
name: Optional[str] = None
|
|
21
22
|
directory: Optional[str] = None
|
|
@@ -34,6 +35,7 @@ class LoggerConfig:
|
|
|
34
35
|
|
|
35
36
|
class LoggerType(str, Enum):
|
|
36
37
|
"""Available logger types"""
|
|
38
|
+
|
|
37
39
|
BASIC = "basic"
|
|
38
40
|
SIZE_ROTATING = "size_rotating"
|
|
39
41
|
TIMED_ROTATING = "timed_rotating"
|
|
@@ -56,12 +58,11 @@ class LoggerFactory:
|
|
|
56
58
|
def _ensure_initialized(cls) -> None:
|
|
57
59
|
"""Ensure memory limits are initialized from settings on first use."""
|
|
58
60
|
if not cls._initialized:
|
|
59
|
-
from pythonLogs.settings import get_log_settings
|
|
60
61
|
settings = get_log_settings()
|
|
61
62
|
cls._max_loggers = settings.max_loggers
|
|
62
63
|
cls._logger_ttl = settings.logger_ttl_seconds
|
|
63
64
|
cls._initialized = True
|
|
64
|
-
|
|
65
|
+
|
|
65
66
|
# Register atexit cleanup on first use
|
|
66
67
|
if not cls._atexit_registered:
|
|
67
68
|
atexit.register(cls._atexit_cleanup)
|
|
@@ -71,30 +72,30 @@ class LoggerFactory:
|
|
|
71
72
|
def get_or_create_logger(
|
|
72
73
|
cls,
|
|
73
74
|
logger_type: Union[LoggerType, str],
|
|
74
|
-
name: Optional[str] = None,
|
|
75
|
+
name: Optional[str] = None,
|
|
76
|
+
**kwargs,
|
|
75
77
|
) -> logging.Logger:
|
|
76
78
|
"""
|
|
77
|
-
Get an existing logger from registry or create new one.
|
|
79
|
+
Get an existing logger from registry or create a new one.
|
|
78
80
|
Loggers are cached by name for performance.
|
|
79
|
-
|
|
81
|
+
|
|
80
82
|
Args:
|
|
81
83
|
logger_type: Type of logger to create
|
|
82
84
|
name: Logger name (used as cache key)
|
|
83
85
|
**kwargs: Additional logger configuration
|
|
84
|
-
|
|
86
|
+
|
|
85
87
|
Returns:
|
|
86
88
|
Cached or newly created logger instance
|
|
87
89
|
"""
|
|
88
90
|
# Use the default name if none provided
|
|
89
91
|
if name is None:
|
|
90
|
-
from pythonLogs.settings import get_log_settings
|
|
91
92
|
name = get_log_settings().appname
|
|
92
93
|
|
|
93
94
|
# Thread-safe check-and-create operation
|
|
94
95
|
with cls._registry_lock:
|
|
95
96
|
# Initialize memory limits from settings on first use
|
|
96
97
|
cls._ensure_initialized()
|
|
97
|
-
|
|
98
|
+
|
|
98
99
|
# Clean up expired loggers first
|
|
99
100
|
cls._cleanup_expired_loggers()
|
|
100
101
|
|
|
@@ -156,7 +157,7 @@ class LoggerFactory:
|
|
|
156
157
|
@classmethod
|
|
157
158
|
def set_memory_limits(cls, max_loggers: int = 100, ttl_seconds: int = 3600) -> None:
|
|
158
159
|
"""Configure memory management limits for the logger registry at runtime.
|
|
159
|
-
|
|
160
|
+
|
|
160
161
|
Args:
|
|
161
162
|
max_loggers: Maximum number of cached loggers
|
|
162
163
|
ttl_seconds: Time-to-live for cached loggers in seconds
|
|
@@ -177,7 +178,7 @@ class LoggerFactory:
|
|
|
177
178
|
except Exception:
|
|
178
179
|
# Silently ignore exceptions during shutdown cleanup
|
|
179
180
|
pass
|
|
180
|
-
|
|
181
|
+
|
|
181
182
|
@staticmethod
|
|
182
183
|
def _cleanup_logger(logger: logging.Logger) -> None:
|
|
183
184
|
"""Clean up logger resources by closing all handlers."""
|
|
@@ -186,10 +187,10 @@ class LoggerFactory:
|
|
|
186
187
|
@classmethod
|
|
187
188
|
def shutdown_logger(cls, name: str) -> bool:
|
|
188
189
|
"""Shutdown and remove a specific logger from registry.
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
Args:
|
|
191
192
|
name: Logger name to shut down
|
|
192
|
-
|
|
193
|
+
|
|
193
194
|
Returns:
|
|
194
195
|
True if logger was found and shutdown, False otherwise
|
|
195
196
|
"""
|
|
@@ -206,24 +207,34 @@ class LoggerFactory:
|
|
|
206
207
|
with cls._registry_lock:
|
|
207
208
|
return {name: logger for name, (logger, _) in cls._logger_registry.items()}
|
|
208
209
|
|
|
210
|
+
@classmethod
|
|
211
|
+
def get_memory_limits(cls) -> dict[str, int]:
|
|
212
|
+
"""Get current memory management limits.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Dictionary with current max_loggers and ttl_seconds settings
|
|
216
|
+
"""
|
|
217
|
+
with cls._registry_lock:
|
|
218
|
+
return {
|
|
219
|
+
'max_loggers': cls._max_loggers,
|
|
220
|
+
'ttl_seconds': cls._logger_ttl
|
|
221
|
+
}
|
|
222
|
+
|
|
209
223
|
@staticmethod
|
|
210
224
|
def create_logger(
|
|
211
|
-
logger_type: Union[LoggerType, str],
|
|
212
|
-
config: Optional[LoggerConfig] = None,
|
|
213
|
-
**kwargs
|
|
225
|
+
logger_type: Union[LoggerType, str], config: Optional[LoggerConfig] = None, **kwargs
|
|
214
226
|
) -> logging.Logger:
|
|
215
|
-
|
|
216
227
|
"""
|
|
217
228
|
Factory method to create loggers based on type.
|
|
218
|
-
|
|
229
|
+
|
|
219
230
|
Args:
|
|
220
231
|
logger_type: Type of logger to create (LoggerType enum or string)
|
|
221
232
|
config: LoggerConfig object with logger parameters
|
|
222
233
|
**kwargs: Individual logger parameters (for backward compatibility)
|
|
223
|
-
|
|
234
|
+
|
|
224
235
|
Returns:
|
|
225
236
|
Configured logger instance
|
|
226
|
-
|
|
237
|
+
|
|
227
238
|
Raises:
|
|
228
239
|
ValueError: If invalid logger_type is provided
|
|
229
240
|
"""
|
|
@@ -237,7 +248,7 @@ class LoggerFactory:
|
|
|
237
248
|
# Merge config and kwargs (kwargs take precedence for backward compatibility)
|
|
238
249
|
if config is None:
|
|
239
250
|
config = LoggerConfig()
|
|
240
|
-
|
|
251
|
+
|
|
241
252
|
# Create a new config with kwargs overriding config values
|
|
242
253
|
final_config = LoggerConfig(
|
|
243
254
|
level=kwargs.get('level', config.level),
|
|
@@ -253,7 +264,7 @@ class LoggerFactory:
|
|
|
253
264
|
when=kwargs.get('when', config.when),
|
|
254
265
|
sufix=kwargs.get('sufix', config.sufix),
|
|
255
266
|
rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
|
|
256
|
-
daystokeep=kwargs.get('daystokeep', config.daystokeep)
|
|
267
|
+
daystokeep=kwargs.get('daystokeep', config.daystokeep),
|
|
257
268
|
)
|
|
258
269
|
|
|
259
270
|
# Convert enum values to strings for logger classes
|
|
@@ -269,7 +280,8 @@ class LoggerFactory:
|
|
|
269
280
|
encoding=final_config.encoding,
|
|
270
281
|
datefmt=final_config.datefmt,
|
|
271
282
|
timezone=final_config.timezone,
|
|
272
|
-
showlocation=final_config.showlocation,
|
|
283
|
+
showlocation=final_config.showlocation,
|
|
284
|
+
)
|
|
273
285
|
|
|
274
286
|
case LoggerType.SIZE_ROTATING:
|
|
275
287
|
logger_instance = SizeRotatingLog(
|
|
@@ -283,7 +295,8 @@ class LoggerFactory:
|
|
|
283
295
|
datefmt=final_config.datefmt,
|
|
284
296
|
timezone=final_config.timezone,
|
|
285
297
|
streamhandler=final_config.streamhandler,
|
|
286
|
-
showlocation=final_config.showlocation,
|
|
298
|
+
showlocation=final_config.showlocation,
|
|
299
|
+
)
|
|
287
300
|
|
|
288
301
|
case LoggerType.TIMED_ROTATING:
|
|
289
302
|
logger_instance = TimedRotatingLog(
|
|
@@ -299,7 +312,8 @@ class LoggerFactory:
|
|
|
299
312
|
timezone=final_config.timezone,
|
|
300
313
|
streamhandler=final_config.streamhandler,
|
|
301
314
|
showlocation=final_config.showlocation,
|
|
302
|
-
rotateatutc=final_config.rotateatutc,
|
|
315
|
+
rotateatutc=final_config.rotateatutc,
|
|
316
|
+
)
|
|
303
317
|
|
|
304
318
|
case _:
|
|
305
319
|
raise ValueError(f"Unsupported logger type: {logger_type}")
|
|
@@ -315,7 +329,6 @@ class LoggerFactory:
|
|
|
315
329
|
timezone: Optional[str] = None,
|
|
316
330
|
showlocation: Optional[bool] = None,
|
|
317
331
|
) -> logging.Logger:
|
|
318
|
-
|
|
319
332
|
"""Convenience method for creating a basic logger"""
|
|
320
333
|
return LoggerFactory.create_logger(
|
|
321
334
|
LoggerType.BASIC,
|
|
@@ -324,7 +337,8 @@ class LoggerFactory:
|
|
|
324
337
|
encoding=encoding,
|
|
325
338
|
datefmt=datefmt,
|
|
326
339
|
timezone=timezone,
|
|
327
|
-
showlocation=showlocation,
|
|
340
|
+
showlocation=showlocation,
|
|
341
|
+
)
|
|
328
342
|
|
|
329
343
|
@staticmethod
|
|
330
344
|
def create_size_rotating_logger(
|
|
@@ -340,7 +354,6 @@ class LoggerFactory:
|
|
|
340
354
|
streamhandler: Optional[bool] = None,
|
|
341
355
|
showlocation: Optional[bool] = None,
|
|
342
356
|
) -> logging.Logger:
|
|
343
|
-
|
|
344
357
|
"""Convenience method for creating a size rotating logger"""
|
|
345
358
|
return LoggerFactory.create_logger(
|
|
346
359
|
LoggerType.SIZE_ROTATING,
|
|
@@ -354,7 +367,8 @@ class LoggerFactory:
|
|
|
354
367
|
datefmt=datefmt,
|
|
355
368
|
timezone=timezone,
|
|
356
369
|
streamhandler=streamhandler,
|
|
357
|
-
showlocation=showlocation,
|
|
370
|
+
showlocation=showlocation,
|
|
371
|
+
)
|
|
358
372
|
|
|
359
373
|
@staticmethod
|
|
360
374
|
def create_timed_rotating_logger(
|
|
@@ -365,14 +379,13 @@ class LoggerFactory:
|
|
|
365
379
|
when: Optional[Union[RotateWhen, str]] = None,
|
|
366
380
|
sufix: Optional[str] = None,
|
|
367
381
|
daystokeep: Optional[int] = None,
|
|
368
|
-
encoding:Optional[str] = None,
|
|
382
|
+
encoding: Optional[str] = None,
|
|
369
383
|
datefmt: Optional[str] = None,
|
|
370
384
|
timezone: Optional[str] = None,
|
|
371
385
|
streamhandler: Optional[bool] = None,
|
|
372
386
|
showlocation: Optional[bool] = None,
|
|
373
387
|
rotateatutc: Optional[bool] = None,
|
|
374
388
|
) -> logging.Logger:
|
|
375
|
-
|
|
376
389
|
"""Convenience method for creating a timed rotating logger"""
|
|
377
390
|
return LoggerFactory.create_logger(
|
|
378
391
|
LoggerType.TIMED_ROTATING,
|
|
@@ -388,7 +401,8 @@ class LoggerFactory:
|
|
|
388
401
|
timezone=timezone,
|
|
389
402
|
streamhandler=streamhandler,
|
|
390
403
|
showlocation=showlocation,
|
|
391
|
-
rotateatutc=rotateatutc,
|
|
404
|
+
rotateatutc=rotateatutc,
|
|
405
|
+
)
|
|
392
406
|
|
|
393
407
|
|
|
394
408
|
# Convenience functions for backward compatibility and easier usage
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import errno
|
|
3
2
|
import gzip
|
|
4
3
|
import logging.handlers
|
|
@@ -10,7 +9,7 @@ import time
|
|
|
10
9
|
from datetime import datetime, timedelta, timezone as dttz
|
|
11
10
|
from functools import lru_cache
|
|
12
11
|
from pathlib import Path
|
|
13
|
-
from typing import Callable, Set
|
|
12
|
+
from typing import Callable, Optional, Set
|
|
14
13
|
from zoneinfo import ZoneInfo
|
|
15
14
|
from pythonLogs.constants import DEFAULT_FILE_MODE, LEVEL_MAP
|
|
16
15
|
|
|
@@ -139,7 +138,7 @@ def is_older_than_x_days(path: str, days: int) -> bool:
|
|
|
139
138
|
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
|
140
139
|
|
|
141
140
|
try:
|
|
142
|
-
if int(days)
|
|
141
|
+
if int(days) == 0:
|
|
143
142
|
cutoff_time = datetime.now()
|
|
144
143
|
else:
|
|
145
144
|
cutoff_time = datetime.now() - timedelta(days=int(days))
|
|
@@ -153,17 +152,21 @@ def is_older_than_x_days(path: str, days: int) -> bool:
|
|
|
153
152
|
|
|
154
153
|
# Cache stderr timezone for better performance
|
|
155
154
|
@lru_cache(maxsize=1)
|
|
156
|
-
def
|
|
155
|
+
def get_stderr_timezone():
|
|
157
156
|
timezone_name = os.getenv("LOG_TIMEZONE", "UTC")
|
|
158
157
|
if timezone_name.lower() == "localtime":
|
|
159
158
|
return None # Use system local timezone
|
|
160
|
-
|
|
159
|
+
try:
|
|
160
|
+
return ZoneInfo(timezone_name)
|
|
161
|
+
except Exception:
|
|
162
|
+
# Fallback to local timezone if requested timezone is not available
|
|
163
|
+
return None
|
|
161
164
|
|
|
162
165
|
|
|
163
166
|
def write_stderr(msg: str) -> None:
|
|
164
167
|
"""Write msg to stderr with optimized timezone handling"""
|
|
165
168
|
try:
|
|
166
|
-
tz =
|
|
169
|
+
tz = get_stderr_timezone()
|
|
167
170
|
if tz is None:
|
|
168
171
|
# Use local timezone
|
|
169
172
|
dt = datetime.now()
|
|
@@ -202,12 +205,17 @@ def get_log_path(directory: str, filename: str) -> str:
|
|
|
202
205
|
|
|
203
206
|
|
|
204
207
|
@lru_cache(maxsize=32)
|
|
205
|
-
def
|
|
206
|
-
"""Cache timezone offset calculation"""
|
|
208
|
+
def get_timezone_offset(timezone_: str) -> str:
|
|
209
|
+
"""Cache timezone offset calculation with fallback for missing timezone data"""
|
|
207
210
|
if timezone_.lower() == "localtime":
|
|
208
211
|
return time.strftime("%z")
|
|
209
212
|
else:
|
|
210
|
-
|
|
213
|
+
try:
|
|
214
|
+
return datetime.now(ZoneInfo(timezone_)).strftime("%z")
|
|
215
|
+
except Exception:
|
|
216
|
+
# Fallback to localtime if the requested timezone is not available,
|
|
217
|
+
# This is common on Windows systems without full timezone data
|
|
218
|
+
return time.strftime("%z")
|
|
211
219
|
|
|
212
220
|
|
|
213
221
|
def get_format(show_location: bool, name: str, timezone_: str) -> str:
|
|
@@ -221,7 +229,7 @@ def get_format(show_location: bool, name: str, timezone_: str) -> str:
|
|
|
221
229
|
if show_location:
|
|
222
230
|
_debug_fmt = "[%(filename)s:%(funcName)s:%(lineno)d]:"
|
|
223
231
|
|
|
224
|
-
utc_offset =
|
|
232
|
+
utc_offset = get_timezone_offset(timezone_)
|
|
225
233
|
return f"[%(asctime)s.%(msecs)03d{utc_offset}]:[%(levelname)s]:{_logger_name}{_debug_fmt}%(message)s"
|
|
226
234
|
|
|
227
235
|
|
|
@@ -235,13 +243,27 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
|
|
|
235
243
|
# Use pathlib for cleaner path operations
|
|
236
244
|
renamed_dst = path_obj.with_name(f"{path_obj.stem}_{sufix}{path_obj.suffix}.gz")
|
|
237
245
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
246
|
+
# Windows-specific retry mechanism for file locking issues
|
|
247
|
+
max_retries = 3 if sys.platform == "win32" else 1
|
|
248
|
+
retry_delay = 0.1 # 100ms delay between retries
|
|
249
|
+
|
|
250
|
+
for attempt in range(max_retries):
|
|
251
|
+
try:
|
|
252
|
+
with open(file_path, "rb") as fin:
|
|
253
|
+
with gzip.open(renamed_dst, "wb", compresslevel=6) as fout: # Balanced compression
|
|
254
|
+
shutil.copyfileobj(fin, fout, length=64 * 1024) # type: ignore # 64KB chunks for better performance
|
|
255
|
+
break # Success, exit retry loop
|
|
256
|
+
except PermissionError as e:
|
|
257
|
+
# Windows file locking issue - retry with delay
|
|
258
|
+
if attempt < max_retries - 1 and sys.platform == "win32":
|
|
259
|
+
time.sleep(retry_delay)
|
|
260
|
+
continue
|
|
261
|
+
# Final attempt failed or not Windows - treat as regular error
|
|
262
|
+
write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
|
|
263
|
+
raise e
|
|
264
|
+
except (OSError, IOError) as e:
|
|
265
|
+
write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
|
|
266
|
+
raise e
|
|
245
267
|
|
|
246
268
|
try:
|
|
247
269
|
path_obj.unlink() # Use pathlib for deletion
|
|
@@ -254,13 +276,84 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
|
|
|
254
276
|
|
|
255
277
|
@lru_cache(maxsize=32)
|
|
256
278
|
def get_timezone_function(time_zone: str) -> Callable:
|
|
257
|
-
"""Get timezone function with caching for
|
|
279
|
+
"""Get timezone function with caching and fallback for missing timezone data"""
|
|
258
280
|
match time_zone.lower():
|
|
259
281
|
case "utc":
|
|
260
|
-
|
|
282
|
+
try:
|
|
283
|
+
# Try to create UTC timezone to verify it's available
|
|
284
|
+
ZoneInfo("UTC")
|
|
285
|
+
return time.gmtime
|
|
286
|
+
except Exception:
|
|
287
|
+
# Fallback to localtime if UTC timezone data is missing
|
|
288
|
+
return time.localtime
|
|
261
289
|
case "localtime":
|
|
262
290
|
return time.localtime
|
|
263
291
|
case _:
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
292
|
+
try:
|
|
293
|
+
# Cache the timezone object
|
|
294
|
+
tz = ZoneInfo(time_zone)
|
|
295
|
+
return lambda *args: datetime.now(tz=tz).timetuple()
|
|
296
|
+
except Exception:
|
|
297
|
+
# Fallback to localtime if the requested timezone is not available
|
|
298
|
+
return time.localtime
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
# Shared handler cleanup utility
|
|
302
|
+
def cleanup_logger_handlers(logger: Optional[logging.Logger]) -> None:
|
|
303
|
+
"""Clean up logger resources by closing all handlers.
|
|
304
|
+
|
|
305
|
+
This is a centralized utility to ensure consistent cleanup behavior
|
|
306
|
+
across all logger types and prevent code duplication.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
logger: The logger to clean up (can be None)
|
|
310
|
+
"""
|
|
311
|
+
if logger is None:
|
|
312
|
+
return
|
|
313
|
+
|
|
314
|
+
# Create a snapshot of handlers to avoid modification during iteration
|
|
315
|
+
handlers_to_remove = list(logger.handlers)
|
|
316
|
+
for handler in handlers_to_remove:
|
|
317
|
+
try:
|
|
318
|
+
handler.close()
|
|
319
|
+
except (OSError, ValueError):
|
|
320
|
+
# Ignore errors during cleanup to prevent cascading failures
|
|
321
|
+
pass
|
|
322
|
+
finally:
|
|
323
|
+
logger.removeHandler(handler)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# Public API for directory cache management
|
|
327
|
+
def set_directory_cache_limit(max_directories: int) -> None:
|
|
328
|
+
"""Set the maximum number of directories to cache.
|
|
329
|
+
|
|
330
|
+
Args:
|
|
331
|
+
max_directories: Maximum number of directories to keep in cache
|
|
332
|
+
"""
|
|
333
|
+
global _max_cached_directories
|
|
334
|
+
|
|
335
|
+
with _directory_lock:
|
|
336
|
+
_max_cached_directories = max_directories
|
|
337
|
+
# Trim cache if it exceeds new limit
|
|
338
|
+
while len(_checked_directories) > max_directories:
|
|
339
|
+
_checked_directories.pop()
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def clear_directory_cache() -> None:
|
|
343
|
+
"""Clear the directory cache to free memory."""
|
|
344
|
+
with _directory_lock:
|
|
345
|
+
_checked_directories.clear()
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def get_directory_cache_stats() -> dict:
|
|
349
|
+
"""Get statistics about the directory cache.
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
Dict with cache statistics including size and limit
|
|
353
|
+
"""
|
|
354
|
+
with _directory_lock:
|
|
355
|
+
return {
|
|
356
|
+
"cached_directories": len(_checked_directories),
|
|
357
|
+
"max_directories": _max_cached_directories,
|
|
358
|
+
"directories": list(_checked_directories)
|
|
359
|
+
}
|
|
@@ -1,34 +1,12 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging
|
|
3
2
|
import threading
|
|
4
3
|
import weakref
|
|
5
4
|
from functools import lru_cache
|
|
6
5
|
from typing import Any, Dict, Optional, Set
|
|
7
6
|
|
|
7
|
+
from . import log_utils
|
|
8
|
+
from .log_utils import cleanup_logger_handlers
|
|
8
9
|
|
|
9
|
-
# Shared handler cleanup utility
|
|
10
|
-
def cleanup_logger_handlers(logger: Optional[logging.Logger]) -> None:
|
|
11
|
-
"""Clean up logger resources by closing all handlers.
|
|
12
|
-
|
|
13
|
-
This is a centralized utility to ensure consistent cleanup behavior
|
|
14
|
-
across all logger types and prevent code duplication.
|
|
15
|
-
|
|
16
|
-
Args:
|
|
17
|
-
logger: The logger to clean up (can be None)
|
|
18
|
-
"""
|
|
19
|
-
if logger is None:
|
|
20
|
-
return
|
|
21
|
-
|
|
22
|
-
# Create a snapshot of handlers to avoid modification during iteration
|
|
23
|
-
handlers_to_remove = list(logger.handlers)
|
|
24
|
-
for handler in handlers_to_remove:
|
|
25
|
-
try:
|
|
26
|
-
handler.close()
|
|
27
|
-
except (OSError, ValueError):
|
|
28
|
-
# Ignore errors during cleanup to prevent cascading failures
|
|
29
|
-
pass
|
|
30
|
-
finally:
|
|
31
|
-
logger.removeHandler(handler)
|
|
32
10
|
|
|
33
11
|
|
|
34
12
|
# Formatter cache to reduce memory usage for identical formatters
|
|
@@ -39,14 +17,14 @@ _max_formatters = 50 # Limit formatter cache size
|
|
|
39
17
|
|
|
40
18
|
def get_cached_formatter(format_string: str, datefmt: Optional[str] = None) -> logging.Formatter:
|
|
41
19
|
"""Get a cached formatter or create and cache a new one.
|
|
42
|
-
|
|
20
|
+
|
|
43
21
|
This reduces memory usage by reusing formatter instances with
|
|
44
22
|
identical configuration instead of creating new ones each time.
|
|
45
|
-
|
|
23
|
+
|
|
46
24
|
Args:
|
|
47
25
|
format_string: The format string for the formatter
|
|
48
26
|
datefmt: Optional date format string
|
|
49
|
-
|
|
27
|
+
|
|
50
28
|
Returns:
|
|
51
29
|
Cached or newly created formatter instance
|
|
52
30
|
"""
|
|
@@ -79,23 +57,16 @@ def clear_formatter_cache() -> None:
|
|
|
79
57
|
# Directory cache utilities with memory management
|
|
80
58
|
def set_directory_cache_limit(max_directories: int) -> None:
|
|
81
59
|
"""Set the maximum number of directories to cache.
|
|
82
|
-
|
|
60
|
+
|
|
83
61
|
Args:
|
|
84
62
|
max_directories: Maximum number of directories to keep in cache
|
|
85
63
|
"""
|
|
86
|
-
|
|
87
|
-
with log_utils._directory_lock:
|
|
88
|
-
log_utils._max_cached_directories = max_directories
|
|
89
|
-
# Trim cache if it exceeds new limit
|
|
90
|
-
while len(log_utils._checked_directories) > max_directories:
|
|
91
|
-
log_utils._checked_directories.pop()
|
|
64
|
+
log_utils.set_directory_cache_limit(max_directories)
|
|
92
65
|
|
|
93
66
|
|
|
94
67
|
def clear_directory_cache() -> None:
|
|
95
68
|
"""Clear the directory cache to free memory."""
|
|
96
|
-
|
|
97
|
-
with log_utils._directory_lock:
|
|
98
|
-
log_utils._checked_directories.clear()
|
|
69
|
+
log_utils.clear_directory_cache()
|
|
99
70
|
|
|
100
71
|
|
|
101
72
|
# Weak reference registry for tracking active loggers without preventing GC
|
|
@@ -105,13 +76,14 @@ _weak_ref_lock = threading.Lock()
|
|
|
105
76
|
|
|
106
77
|
def register_logger_weakref(logger: logging.Logger) -> None:
|
|
107
78
|
"""Register a weak reference to a logger for memory tracking.
|
|
108
|
-
|
|
79
|
+
|
|
109
80
|
This allows monitoring active loggers without preventing garbage collection.
|
|
110
|
-
|
|
81
|
+
|
|
111
82
|
Args:
|
|
112
83
|
logger: Logger to track
|
|
113
84
|
"""
|
|
114
85
|
global _active_loggers
|
|
86
|
+
|
|
115
87
|
def cleanup_callback(ref):
|
|
116
88
|
with _weak_ref_lock:
|
|
117
89
|
_active_loggers.discard(ref)
|
|
@@ -123,7 +95,7 @@ def register_logger_weakref(logger: logging.Logger) -> None:
|
|
|
123
95
|
|
|
124
96
|
def get_active_logger_count() -> int:
|
|
125
97
|
"""Get the count of currently active loggers.
|
|
126
|
-
|
|
98
|
+
|
|
127
99
|
Returns:
|
|
128
100
|
Number of active logger instances
|
|
129
101
|
"""
|
|
@@ -137,30 +109,33 @@ def get_active_logger_count() -> int:
|
|
|
137
109
|
|
|
138
110
|
def get_memory_stats() -> Dict[str, Any]:
|
|
139
111
|
"""Get memory usage statistics for the logging system.
|
|
140
|
-
|
|
112
|
+
|
|
141
113
|
Returns:
|
|
142
114
|
Dictionary containing memory usage statistics
|
|
143
115
|
"""
|
|
144
116
|
from . import factory
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
117
|
+
|
|
118
|
+
# Get registry stats using public API
|
|
119
|
+
registered_loggers = factory.LoggerFactory.get_registered_loggers()
|
|
120
|
+
registry_size = len(registered_loggers)
|
|
121
|
+
|
|
122
|
+
# Get memory limits using public API
|
|
123
|
+
factory_limits = factory.LoggerFactory.get_memory_limits()
|
|
148
124
|
|
|
149
125
|
with _formatter_cache_lock:
|
|
150
126
|
formatter_cache_size = len(_formatter_cache)
|
|
151
127
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
directory_cache_size = len(log_utils._checked_directories)
|
|
128
|
+
# Get directory cache stats using public API
|
|
129
|
+
directory_stats = log_utils.get_directory_cache_stats()
|
|
155
130
|
|
|
156
131
|
return {
|
|
157
132
|
'registry_size': registry_size,
|
|
158
133
|
'formatter_cache_size': formatter_cache_size,
|
|
159
|
-
'directory_cache_size':
|
|
134
|
+
'directory_cache_size': directory_stats['cached_directories'],
|
|
160
135
|
'active_logger_count': get_active_logger_count(),
|
|
161
|
-
'max_registry_size':
|
|
136
|
+
'max_registry_size': factory_limits['max_loggers'],
|
|
162
137
|
'max_formatter_cache': _max_formatters,
|
|
163
|
-
'max_directory_cache':
|
|
138
|
+
'max_directory_cache': directory_stats['max_directories'],
|
|
164
139
|
}
|
|
165
140
|
|
|
166
141
|
|
|
@@ -168,33 +143,32 @@ def get_memory_stats() -> Dict[str, Any]:
|
|
|
168
143
|
def optimize_lru_cache_sizes() -> None:
|
|
169
144
|
"""Optimize LRU cache sizes based on typical usage patterns."""
|
|
170
145
|
# Clear existing caches and reduce their sizes
|
|
171
|
-
|
|
172
|
-
|
|
146
|
+
|
|
173
147
|
# Clear and recreate timezone function cache with smaller size
|
|
174
148
|
log_utils.get_timezone_function.cache_clear()
|
|
175
149
|
log_utils.get_timezone_function = lru_cache(maxsize=8)(log_utils.get_timezone_function.__wrapped__)
|
|
176
150
|
|
|
177
|
-
# Clear and recreate timezone offset cache with smaller size
|
|
178
|
-
log_utils.
|
|
179
|
-
log_utils.
|
|
151
|
+
# Clear and recreate timezone offset cache with smaller size
|
|
152
|
+
log_utils.get_timezone_offset.cache_clear()
|
|
153
|
+
log_utils.get_timezone_offset = lru_cache(maxsize=8)(log_utils.get_timezone_offset.__wrapped__)
|
|
180
154
|
|
|
181
155
|
# Clear and recreate stderr timezone cache with smaller size
|
|
182
|
-
log_utils.
|
|
183
|
-
log_utils.
|
|
156
|
+
log_utils.get_stderr_timezone.cache_clear()
|
|
157
|
+
log_utils.get_stderr_timezone = lru_cache(maxsize=4)(log_utils.get_stderr_timezone.__wrapped__)
|
|
184
158
|
|
|
185
159
|
|
|
186
160
|
def force_garbage_collection() -> Dict[str, int]:
|
|
187
161
|
"""Force garbage collection and return collection statistics.
|
|
188
|
-
|
|
162
|
+
|
|
189
163
|
This can be useful for testing memory leaks or forcing cleanup
|
|
190
164
|
in long-running applications.
|
|
191
|
-
|
|
165
|
+
|
|
192
166
|
Returns:
|
|
193
167
|
Dictionary with garbage collection statistics
|
|
194
168
|
"""
|
|
195
169
|
import gc
|
|
196
170
|
|
|
197
|
-
# Clear all our caches first
|
|
171
|
+
# Clear all our caches first using public APIs
|
|
198
172
|
clear_formatter_cache()
|
|
199
173
|
clear_directory_cache()
|
|
200
174
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
from functools import lru_cache
|
|
3
2
|
from typing import Optional
|
|
4
3
|
from dotenv import load_dotenv
|
|
@@ -27,7 +26,7 @@ class LogSettings(BaseSettings):
|
|
|
27
26
|
encoding: Optional[str] = Field(default=DEFAULT_ENCODING)
|
|
28
27
|
appname: Optional[str] = Field(default="app")
|
|
29
28
|
filename: Optional[str] = Field(default="app.log")
|
|
30
|
-
directory: Optional[str] = Field(default="
|
|
29
|
+
directory: Optional[str] = Field(default="./logs")
|
|
31
30
|
days_to_keep: Optional[int] = Field(default=DEFAULT_BACKUP_COUNT)
|
|
32
31
|
date_format: Optional[str] = Field(default=DEFAULT_DATE_FORMAT)
|
|
33
32
|
stream_handler: Optional[bool] = Field(default=True)
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging.handlers
|
|
3
2
|
import os
|
|
4
3
|
import re
|
|
4
|
+
from pathlib import Path
|
|
5
5
|
from typing import Optional
|
|
6
6
|
from pythonLogs.constants import MB_TO_BYTES
|
|
7
7
|
from pythonLogs.log_utils import (
|
|
8
8
|
check_directory_permissions,
|
|
9
9
|
check_filename_instance,
|
|
10
|
+
cleanup_logger_handlers,
|
|
10
11
|
get_level,
|
|
11
12
|
get_log_path,
|
|
12
13
|
get_logger_and_formatter,
|
|
@@ -15,7 +16,7 @@ from pythonLogs.log_utils import (
|
|
|
15
16
|
remove_old_logs,
|
|
16
17
|
write_stderr,
|
|
17
18
|
)
|
|
18
|
-
from pythonLogs.memory_utils import
|
|
19
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
19
20
|
from pythonLogs.settings import get_log_settings
|
|
20
21
|
from pythonLogs.thread_safety import auto_thread_safe
|
|
21
22
|
|
|
@@ -23,6 +24,7 @@ from pythonLogs.thread_safety import auto_thread_safe
|
|
|
23
24
|
@auto_thread_safe(['init', '_cleanup_logger'])
|
|
24
25
|
class SizeRotatingLog:
|
|
25
26
|
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
|
|
27
|
+
|
|
26
28
|
def __init__(
|
|
27
29
|
self,
|
|
28
30
|
level: Optional[str] = None,
|
|
@@ -83,22 +85,22 @@ class SizeRotatingLog:
|
|
|
83
85
|
# Register weak reference for memory tracking
|
|
84
86
|
register_logger_weakref(logger)
|
|
85
87
|
return logger
|
|
86
|
-
|
|
88
|
+
|
|
87
89
|
def __enter__(self):
|
|
88
90
|
"""Context manager entry."""
|
|
89
91
|
if not hasattr(self, 'logger') or self.logger is None:
|
|
90
92
|
self.init()
|
|
91
93
|
return self.logger
|
|
92
|
-
|
|
94
|
+
|
|
93
95
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
94
96
|
"""Context manager exit with automatic cleanup."""
|
|
95
97
|
if hasattr(self, 'logger'):
|
|
96
98
|
self._cleanup_logger(self.logger)
|
|
97
|
-
|
|
99
|
+
|
|
98
100
|
def _cleanup_logger(self, logger: logging.Logger) -> None:
|
|
99
101
|
"""Clean up logger resources by closing all handlers with thread safety."""
|
|
100
102
|
cleanup_logger_handlers(logger)
|
|
101
|
-
|
|
103
|
+
|
|
102
104
|
@staticmethod
|
|
103
105
|
def cleanup_logger(logger: logging.Logger) -> None:
|
|
104
106
|
"""Static method for cleaning up logger resources (backward compatibility)."""
|
|
@@ -124,7 +126,6 @@ class GZipRotatorSize:
|
|
|
124
126
|
max_num = 0
|
|
125
127
|
try:
|
|
126
128
|
# Use pathlib for better performance with large directories
|
|
127
|
-
from pathlib import Path
|
|
128
129
|
dir_path = Path(directory)
|
|
129
130
|
for file_path in dir_path.iterdir():
|
|
130
131
|
if file_path.is_file():
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import functools
|
|
3
2
|
import threading
|
|
4
3
|
from typing import Any, Callable, Dict, Type, TypeVar
|
|
@@ -9,26 +8,29 @@ F = TypeVar('F', bound=Callable[..., Any])
|
|
|
9
8
|
|
|
10
9
|
class ThreadSafeMeta(type):
|
|
11
10
|
"""Metaclass that automatically adds thread safety to class methods."""
|
|
12
|
-
|
|
11
|
+
|
|
13
12
|
def __new__(mcs, name: str, bases: tuple, namespace: Dict[str, Any], **kwargs):
|
|
14
13
|
# Create the class first
|
|
15
14
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
16
|
-
|
|
15
|
+
|
|
17
16
|
# Add a class-level lock if not already present
|
|
18
17
|
if not hasattr(cls, '_lock'):
|
|
19
18
|
cls._lock = threading.RLock()
|
|
20
|
-
|
|
19
|
+
|
|
21
20
|
# Get methods that should be thread-safe (exclude private/dunder methods)
|
|
22
21
|
thread_safe_methods = getattr(cls, '_thread_safe_methods', None)
|
|
23
22
|
if thread_safe_methods is None:
|
|
24
23
|
# Auto-detect public methods that modify state
|
|
25
24
|
thread_safe_methods = [
|
|
26
|
-
method_name
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
)
|
|
30
32
|
]
|
|
31
|
-
|
|
33
|
+
|
|
32
34
|
# Wrap each method with automatic locking
|
|
33
35
|
for method_name in thread_safe_methods:
|
|
34
36
|
if hasattr(cls, method_name):
|
|
@@ -36,13 +38,13 @@ class ThreadSafeMeta(type):
|
|
|
36
38
|
if callable(original_method):
|
|
37
39
|
wrapped_method = thread_safe(original_method)
|
|
38
40
|
setattr(cls, method_name, wrapped_method)
|
|
39
|
-
|
|
41
|
+
|
|
40
42
|
return cls
|
|
41
43
|
|
|
42
44
|
|
|
43
45
|
def thread_safe(func: F) -> F:
|
|
44
46
|
"""Decorator that automatically adds thread safety to methods."""
|
|
45
|
-
|
|
47
|
+
|
|
46
48
|
@functools.wraps(func)
|
|
47
49
|
def wrapper(self, *args, **kwargs):
|
|
48
50
|
# Use instance lock if available, otherwise class lock
|
|
@@ -52,20 +54,23 @@ def thread_safe(func: F) -> F:
|
|
|
52
54
|
if not hasattr(self.__class__, '_lock'):
|
|
53
55
|
self.__class__._lock = threading.RLock()
|
|
54
56
|
lock = self.__class__._lock
|
|
55
|
-
|
|
57
|
+
|
|
56
58
|
with lock:
|
|
57
59
|
return func(self, *args, **kwargs)
|
|
58
|
-
|
|
60
|
+
|
|
59
61
|
return wrapper
|
|
60
62
|
|
|
61
63
|
|
|
62
64
|
def _get_wrappable_methods(cls: Type) -> list:
|
|
63
65
|
"""Helper function to get methods that should be made thread-safe."""
|
|
64
66
|
return [
|
|
65
|
-
method_name
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
+
)
|
|
69
74
|
]
|
|
70
75
|
|
|
71
76
|
|
|
@@ -77,24 +82,24 @@ def _ensure_class_has_lock(cls: Type) -> None:
|
|
|
77
82
|
|
|
78
83
|
def _should_wrap_method(cls: Type, method_name: str, original_method: Any) -> bool:
|
|
79
84
|
"""Check if a method should be wrapped with thread safety."""
|
|
80
|
-
return (
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
return (
|
|
86
|
+
hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, '_thread_safe_wrapped')
|
|
87
|
+
)
|
|
83
88
|
|
|
84
89
|
|
|
85
90
|
def auto_thread_safe(thread_safe_methods: list = None):
|
|
86
91
|
"""Class decorator that adds automatic thread safety to specified methods."""
|
|
87
|
-
|
|
92
|
+
|
|
88
93
|
def decorator(cls: Type) -> Type:
|
|
89
94
|
_ensure_class_has_lock(cls)
|
|
90
|
-
|
|
95
|
+
|
|
91
96
|
# Store thread-safe methods list
|
|
92
97
|
if thread_safe_methods:
|
|
93
98
|
cls._thread_safe_methods = thread_safe_methods
|
|
94
|
-
|
|
99
|
+
|
|
95
100
|
# Get methods to make thread-safe
|
|
96
101
|
methods_to_wrap = thread_safe_methods or _get_wrappable_methods(cls)
|
|
97
|
-
|
|
102
|
+
|
|
98
103
|
# Wrap each method
|
|
99
104
|
for method_name in methods_to_wrap:
|
|
100
105
|
original_method = getattr(cls, method_name, None)
|
|
@@ -102,26 +107,26 @@ def auto_thread_safe(thread_safe_methods: list = None):
|
|
|
102
107
|
wrapped_method = thread_safe(original_method)
|
|
103
108
|
wrapped_method._thread_safe_wrapped = True
|
|
104
109
|
setattr(cls, method_name, wrapped_method)
|
|
105
|
-
|
|
110
|
+
|
|
106
111
|
return cls
|
|
107
|
-
|
|
112
|
+
|
|
108
113
|
return decorator
|
|
109
114
|
|
|
110
115
|
|
|
111
116
|
class AutoThreadSafe:
|
|
112
117
|
"""Base class that provides automatic thread safety for all public methods."""
|
|
113
|
-
|
|
118
|
+
|
|
114
119
|
def __init__(self):
|
|
115
120
|
if not hasattr(self, '_lock'):
|
|
116
121
|
self._lock = threading.RLock()
|
|
117
|
-
|
|
122
|
+
|
|
118
123
|
def __init_subclass__(cls, **kwargs):
|
|
119
124
|
super().__init_subclass__(**kwargs)
|
|
120
|
-
|
|
125
|
+
|
|
121
126
|
# Add class-level lock
|
|
122
127
|
if not hasattr(cls, '_lock'):
|
|
123
128
|
cls._lock = threading.RLock()
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
# Auto-wrap public methods
|
|
126
131
|
for attr_name in dir(cls):
|
|
127
132
|
if not attr_name.startswith('_'):
|
|
@@ -139,13 +144,13 @@ def synchronized_method(func: F) -> F:
|
|
|
139
144
|
|
|
140
145
|
class ThreadSafeContext:
|
|
141
146
|
"""Context manager for thread-safe operations."""
|
|
142
|
-
|
|
147
|
+
|
|
143
148
|
def __init__(self, lock: threading.Lock):
|
|
144
149
|
self.lock = lock
|
|
145
|
-
|
|
150
|
+
|
|
146
151
|
def __enter__(self):
|
|
147
152
|
self.lock.acquire()
|
|
148
153
|
return self
|
|
149
|
-
|
|
154
|
+
|
|
150
155
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
151
156
|
self.lock.release()
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
# -*- encoding: utf-8 -*-
|
|
2
1
|
import logging.handlers
|
|
3
2
|
import os
|
|
4
3
|
from typing import Optional
|
|
5
4
|
from pythonLogs.log_utils import (
|
|
6
5
|
check_directory_permissions,
|
|
7
6
|
check_filename_instance,
|
|
7
|
+
cleanup_logger_handlers,
|
|
8
8
|
get_level,
|
|
9
9
|
get_log_path,
|
|
10
10
|
get_logger_and_formatter,
|
|
11
11
|
get_stream_handler,
|
|
12
12
|
gzip_file_with_sufix,
|
|
13
|
-
|
|
13
|
+
remove_old_logs,
|
|
14
14
|
)
|
|
15
|
-
from pythonLogs.memory_utils import
|
|
15
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
16
16
|
from pythonLogs.settings import get_log_settings
|
|
17
17
|
from pythonLogs.thread_safety import auto_thread_safe
|
|
18
18
|
|
|
@@ -21,7 +21,7 @@ from pythonLogs.thread_safety import auto_thread_safe
|
|
|
21
21
|
class TimedRotatingLog:
|
|
22
22
|
"""
|
|
23
23
|
Time-based rotating logger with context manager support for automatic resource cleanup.
|
|
24
|
-
|
|
24
|
+
|
|
25
25
|
Current 'rotating_when' events supported for TimedRotatingLogs:
|
|
26
26
|
Use RotateWhen enum values:
|
|
27
27
|
RotateWhen.MIDNIGHT - roll over at midnight
|
|
@@ -40,11 +40,11 @@ class TimedRotatingLog:
|
|
|
40
40
|
sufix: Optional[str] = None,
|
|
41
41
|
daystokeep: Optional[int] = None,
|
|
42
42
|
encoding: Optional[str] = None,
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
48
|
):
|
|
49
49
|
_settings = get_log_settings()
|
|
50
50
|
self.level = get_level(level or _settings.level)
|
|
@@ -77,7 +77,8 @@ class TimedRotatingLog:
|
|
|
77
77
|
encoding=self.encoding,
|
|
78
78
|
when=self.when,
|
|
79
79
|
utc=self.rotateatutc,
|
|
80
|
-
backupCount=self.daystokeep,
|
|
80
|
+
backupCount=self.daystokeep,
|
|
81
|
+
)
|
|
81
82
|
file_handler.suffix = self.sufix
|
|
82
83
|
file_handler.rotator = GZipRotatorTimed(self.directory, self.daystokeep)
|
|
83
84
|
file_handler.setFormatter(formatter)
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
LOG_LEVEL=DEBUG
|
|
2
|
-
LOG_TIMEZONE=UTC
|
|
3
|
-
LOG_ENCODING=UTF-8
|
|
4
|
-
LOG_APPNAME=app
|
|
5
|
-
LOG_FILENAME=app.log
|
|
6
|
-
LOG_DIRECTORY=/app/logs
|
|
7
|
-
LOG_DAYS_TO_KEEP=30
|
|
8
|
-
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
|
|
9
|
-
LOG_STREAM_HANDLER=True
|
|
10
|
-
LOG_SHOW_LOCATION=False
|
|
11
|
-
LOG_MAX_LOGGERS=50
|
|
12
|
-
LOG_LOGGER_TTL_SECONDS=1800
|
|
13
|
-
|
|
14
|
-
# SizeRotatingLog
|
|
15
|
-
LOG_MAX_FILE_SIZE_MB=10
|
|
16
|
-
|
|
17
|
-
# TimedRotatingLog
|
|
18
|
-
LOG_ROTATE_WHEN=midnight
|
|
19
|
-
LOG_ROTATE_AT_UTC=True
|
|
20
|
-
LOG_ROTATE_FILE_SUFIX="%Y%m%d"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|