pythonLogs 5.0.1__cp312-cp312-win_amd64.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.1.dist-info/LICENSE +21 -0
- pythonlogs-5.0.1.dist-info/METADATA +576 -0
- pythonlogs-5.0.1.dist-info/RECORD +15 -0
- pythonlogs-5.0.1.dist-info/WHEEL +4 -0
pythonLogs/.env.example
ADDED
|
@@ -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
|
pythonLogs/__init__.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from typing import Literal, NamedTuple
|
|
4
|
+
from pythonLogs.basic_log import BasicLog
|
|
5
|
+
from pythonLogs.constants import LogLevel, RotateWhen
|
|
6
|
+
from pythonLogs.factory import (
|
|
7
|
+
basic_logger,
|
|
8
|
+
clear_logger_registry,
|
|
9
|
+
create_logger,
|
|
10
|
+
get_or_create_logger,
|
|
11
|
+
get_registered_loggers,
|
|
12
|
+
LoggerFactory,
|
|
13
|
+
LoggerType,
|
|
14
|
+
shutdown_logger,
|
|
15
|
+
size_rotating_logger,
|
|
16
|
+
timed_rotating_logger,
|
|
17
|
+
)
|
|
18
|
+
from pythonLogs.memory_utils import (
|
|
19
|
+
clear_directory_cache,
|
|
20
|
+
clear_formatter_cache,
|
|
21
|
+
force_garbage_collection,
|
|
22
|
+
get_memory_stats,
|
|
23
|
+
optimize_lru_cache_sizes,
|
|
24
|
+
set_directory_cache_limit,
|
|
25
|
+
)
|
|
26
|
+
from pythonLogs.size_rotating import SizeRotatingLog
|
|
27
|
+
from pythonLogs.timed_rotating import TimedRotatingLog
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
__all__ = (
|
|
31
|
+
"BasicLog",
|
|
32
|
+
"TimedRotatingLog",
|
|
33
|
+
"SizeRotatingLog",
|
|
34
|
+
"LoggerFactory",
|
|
35
|
+
"LoggerType",
|
|
36
|
+
"LogLevel",
|
|
37
|
+
"RotateWhen",
|
|
38
|
+
"create_logger",
|
|
39
|
+
"get_or_create_logger",
|
|
40
|
+
"basic_logger",
|
|
41
|
+
"size_rotating_logger",
|
|
42
|
+
"timed_rotating_logger",
|
|
43
|
+
"clear_logger_registry",
|
|
44
|
+
"get_registered_loggers",
|
|
45
|
+
"shutdown_logger",
|
|
46
|
+
# Memory management utilities
|
|
47
|
+
"get_memory_stats",
|
|
48
|
+
"clear_formatter_cache",
|
|
49
|
+
"clear_directory_cache",
|
|
50
|
+
"force_garbage_collection",
|
|
51
|
+
"optimize_lru_cache_sizes",
|
|
52
|
+
"set_directory_cache_limit",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
__title__ = "pythonLogs"
|
|
56
|
+
__author__ = "Daniel Costa"
|
|
57
|
+
__email__ = "danieldcsta@gmail.com>"
|
|
58
|
+
__license__ = "MIT"
|
|
59
|
+
__copyright__ = "Copyright 2024-present ddc"
|
|
60
|
+
_req_python_version = (3, 12, 0)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
_version = tuple(int(x) for x in version(__title__).split("."))
|
|
65
|
+
except ModuleNotFoundError:
|
|
66
|
+
_version = (0, 0, 0)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class VersionInfo(NamedTuple):
|
|
70
|
+
major: int
|
|
71
|
+
minor: int
|
|
72
|
+
micro: int
|
|
73
|
+
releaselevel: Literal["alpha", "beta", "candidate", "final"]
|
|
74
|
+
serial: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
__version__ = _version
|
|
78
|
+
__version_info__: VersionInfo = VersionInfo(
|
|
79
|
+
major=__version__[0], minor=__version__[1], micro=__version__[2], releaselevel="final", serial=0
|
|
80
|
+
)
|
|
81
|
+
__req_python_version__: VersionInfo = VersionInfo(
|
|
82
|
+
major=_req_python_version[0],
|
|
83
|
+
minor=_req_python_version[1],
|
|
84
|
+
micro=_req_python_version[2],
|
|
85
|
+
releaselevel="final",
|
|
86
|
+
serial=0,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
90
|
+
|
|
91
|
+
del logging, NamedTuple, Literal, VersionInfo, version, _version, _req_python_version
|
pythonLogs/basic_log.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pythonLogs.log_utils import get_format, get_level, get_timezone_function
|
|
4
|
+
from pythonLogs.log_utils import cleanup_logger_handlers
|
|
5
|
+
from pythonLogs.memory_utils import register_logger_weakref
|
|
6
|
+
from pythonLogs.settings import get_log_settings
|
|
7
|
+
from pythonLogs.thread_safety import auto_thread_safe
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@auto_thread_safe(['init', '_cleanup_logger'])
|
|
11
|
+
class BasicLog:
|
|
12
|
+
"""Basic logger with context manager support for automatic resource cleanup."""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
level: Optional[str] = None,
|
|
17
|
+
name: Optional[str] = None,
|
|
18
|
+
encoding: Optional[str] = None,
|
|
19
|
+
datefmt: Optional[str] = None,
|
|
20
|
+
timezone: Optional[str] = None,
|
|
21
|
+
showlocation: Optional[bool] = None,
|
|
22
|
+
):
|
|
23
|
+
_settings = get_log_settings()
|
|
24
|
+
self.level = get_level(level or _settings.level)
|
|
25
|
+
self.appname = name or _settings.appname
|
|
26
|
+
self.encoding = encoding or _settings.encoding
|
|
27
|
+
self.datefmt = datefmt or _settings.date_format
|
|
28
|
+
self.timezone = timezone or _settings.timezone
|
|
29
|
+
self.showlocation = showlocation or _settings.show_location
|
|
30
|
+
self.logger = None
|
|
31
|
+
|
|
32
|
+
def init(self):
|
|
33
|
+
logger = logging.getLogger(self.appname)
|
|
34
|
+
logger.setLevel(self.level)
|
|
35
|
+
logging.Formatter.converter = get_timezone_function(self.timezone)
|
|
36
|
+
_format = get_format(self.showlocation, self.appname, self.timezone)
|
|
37
|
+
|
|
38
|
+
# Only add handler if logger doesn't have any handlers
|
|
39
|
+
if not logger.handlers:
|
|
40
|
+
handler = logging.StreamHandler()
|
|
41
|
+
formatter = logging.Formatter(_format, datefmt=self.datefmt)
|
|
42
|
+
handler.setFormatter(formatter)
|
|
43
|
+
logger.addHandler(handler)
|
|
44
|
+
|
|
45
|
+
self.logger = logger
|
|
46
|
+
# Register weak reference for memory tracking
|
|
47
|
+
register_logger_weakref(logger)
|
|
48
|
+
return logger
|
|
49
|
+
|
|
50
|
+
def __enter__(self):
|
|
51
|
+
"""Context manager entry."""
|
|
52
|
+
if not hasattr(self, 'logger') or self.logger is None:
|
|
53
|
+
self.init()
|
|
54
|
+
return self.logger
|
|
55
|
+
|
|
56
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
57
|
+
"""Context manager exit with automatic cleanup."""
|
|
58
|
+
if hasattr(self, 'logger'):
|
|
59
|
+
self._cleanup_logger(self.logger)
|
|
60
|
+
|
|
61
|
+
def _cleanup_logger(self, logger: logging.Logger) -> None:
|
|
62
|
+
"""Clean up logger resources by closing all handlers with thread safety."""
|
|
63
|
+
cleanup_logger_handlers(logger)
|
|
64
|
+
|
|
65
|
+
@staticmethod
|
|
66
|
+
def cleanup_logger(logger: logging.Logger) -> None:
|
|
67
|
+
"""Static method for cleaning up logger resources (backward compatibility)."""
|
|
68
|
+
cleanup_logger_handlers(logger)
|
pythonLogs/constants.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
|
|
4
|
+
# File and Directory Constants
|
|
5
|
+
MB_TO_BYTES = 1024 * 1024
|
|
6
|
+
DEFAULT_FILE_MODE = 0o755
|
|
7
|
+
DEFAULT_BACKUP_COUNT = 30
|
|
8
|
+
|
|
9
|
+
# Date Format Constants
|
|
10
|
+
DEFAULT_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
|
|
11
|
+
DEFAULT_ROTATE_SUFFIX = "%Y%m%d"
|
|
12
|
+
|
|
13
|
+
# Encoding Constants
|
|
14
|
+
DEFAULT_ENCODING = "UTF-8"
|
|
15
|
+
|
|
16
|
+
# Timezone Constants
|
|
17
|
+
DEFAULT_TIMEZONE = "UTC"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LogLevel(str, Enum):
|
|
21
|
+
"""Log levels"""
|
|
22
|
+
|
|
23
|
+
CRITICAL = "CRITICAL"
|
|
24
|
+
CRIT = "CRIT"
|
|
25
|
+
ERROR = "ERROR"
|
|
26
|
+
WARNING = "WARNING"
|
|
27
|
+
WARN = "WARN"
|
|
28
|
+
INFO = "INFO"
|
|
29
|
+
DEBUG = "DEBUG"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class RotateWhen(str, Enum):
|
|
33
|
+
"""Rotation timing options for TimedRotatingLog"""
|
|
34
|
+
|
|
35
|
+
MIDNIGHT = "midnight"
|
|
36
|
+
MONDAY = "W0"
|
|
37
|
+
TUESDAY = "W1"
|
|
38
|
+
WEDNESDAY = "W2"
|
|
39
|
+
THURSDAY = "W3"
|
|
40
|
+
FRIDAY = "W4"
|
|
41
|
+
SATURDAY = "W5"
|
|
42
|
+
SUNDAY = "W6"
|
|
43
|
+
HOURLY = "H"
|
|
44
|
+
DAILY = "D"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Level mapping for performance optimization
|
|
48
|
+
LEVEL_MAP = {
|
|
49
|
+
LogLevel.DEBUG.value.lower(): logging.DEBUG,
|
|
50
|
+
LogLevel.WARNING.value.lower(): logging.WARNING,
|
|
51
|
+
LogLevel.WARN.value.lower(): logging.WARNING,
|
|
52
|
+
LogLevel.ERROR.value.lower(): logging.ERROR,
|
|
53
|
+
LogLevel.CRITICAL.value.lower(): logging.CRITICAL,
|
|
54
|
+
LogLevel.CRIT.value.lower(): logging.CRITICAL,
|
|
55
|
+
LogLevel.INFO.value.lower(): logging.INFO,
|
|
56
|
+
}
|
pythonLogs/factory.py
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
import atexit
|
|
2
|
+
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from typing import Dict, Optional, Tuple, Union
|
|
8
|
+
from pythonLogs.basic_log import BasicLog
|
|
9
|
+
from pythonLogs.constants import LogLevel, RotateWhen
|
|
10
|
+
from pythonLogs.log_utils import cleanup_logger_handlers
|
|
11
|
+
from pythonLogs.settings import get_log_settings
|
|
12
|
+
from pythonLogs.size_rotating import SizeRotatingLog
|
|
13
|
+
from pythonLogs.timed_rotating import TimedRotatingLog
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class LoggerConfig:
|
|
18
|
+
"""Configuration class to group logger parameters"""
|
|
19
|
+
|
|
20
|
+
level: Optional[Union[LogLevel, str]] = None
|
|
21
|
+
name: Optional[str] = None
|
|
22
|
+
directory: Optional[str] = None
|
|
23
|
+
filenames: Optional[list | tuple] = None
|
|
24
|
+
encoding: Optional[str] = None
|
|
25
|
+
datefmt: Optional[str] = None
|
|
26
|
+
timezone: Optional[str] = None
|
|
27
|
+
streamhandler: Optional[bool] = None
|
|
28
|
+
showlocation: Optional[bool] = None
|
|
29
|
+
maxmbytes: Optional[int] = None
|
|
30
|
+
when: Optional[Union[RotateWhen, str]] = None
|
|
31
|
+
sufix: Optional[str] = None
|
|
32
|
+
rotateatutc: Optional[bool] = None
|
|
33
|
+
daystokeep: Optional[int] = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LoggerType(str, Enum):
|
|
37
|
+
"""Available logger types"""
|
|
38
|
+
|
|
39
|
+
BASIC = "basic"
|
|
40
|
+
SIZE_ROTATING = "size_rotating"
|
|
41
|
+
TIMED_ROTATING = "timed_rotating"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class LoggerFactory:
|
|
45
|
+
"""Factory for creating different types of loggers with optimized instantiation and memory management"""
|
|
46
|
+
|
|
47
|
+
# Logger registry for reusing loggers by name with timestamp tracking
|
|
48
|
+
_logger_registry: Dict[str, Tuple[logging.Logger, float]] = {}
|
|
49
|
+
# Thread lock for registry access
|
|
50
|
+
_registry_lock = threading.RLock()
|
|
51
|
+
# Memory optimization settings
|
|
52
|
+
_max_loggers = 100 # Maximum number of cached loggers
|
|
53
|
+
_logger_ttl = 3600 # Logger TTL in seconds (1 hour)
|
|
54
|
+
_initialized = False # Flag to track if memory limits have been initialized
|
|
55
|
+
_atexit_registered = False # Flag to track if atexit cleanup is registered
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def _ensure_initialized(cls) -> None:
|
|
59
|
+
"""Ensure memory limits are initialized from settings on first use."""
|
|
60
|
+
if not cls._initialized:
|
|
61
|
+
settings = get_log_settings()
|
|
62
|
+
cls._max_loggers = settings.max_loggers
|
|
63
|
+
cls._logger_ttl = settings.logger_ttl_seconds
|
|
64
|
+
cls._initialized = True
|
|
65
|
+
|
|
66
|
+
# Register atexit cleanup on first use
|
|
67
|
+
if not cls._atexit_registered:
|
|
68
|
+
atexit.register(cls._atexit_cleanup)
|
|
69
|
+
cls._atexit_registered = True
|
|
70
|
+
|
|
71
|
+
@classmethod
|
|
72
|
+
def get_or_create_logger(
|
|
73
|
+
cls,
|
|
74
|
+
logger_type: Union[LoggerType, str],
|
|
75
|
+
name: Optional[str] = None,
|
|
76
|
+
**kwargs,
|
|
77
|
+
) -> logging.Logger:
|
|
78
|
+
"""
|
|
79
|
+
Get an existing logger from registry or create a new one.
|
|
80
|
+
Loggers are cached by name for performance.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
logger_type: Type of logger to create
|
|
84
|
+
name: Logger name (used as cache key)
|
|
85
|
+
**kwargs: Additional logger configuration
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
Cached or newly created logger instance
|
|
89
|
+
"""
|
|
90
|
+
# Use the default name if none provided
|
|
91
|
+
if name is None:
|
|
92
|
+
name = get_log_settings().appname
|
|
93
|
+
|
|
94
|
+
# Thread-safe check-and-create operation
|
|
95
|
+
with cls._registry_lock:
|
|
96
|
+
# Initialize memory limits from settings on first use
|
|
97
|
+
cls._ensure_initialized()
|
|
98
|
+
|
|
99
|
+
# Clean up expired loggers first
|
|
100
|
+
cls._cleanup_expired_loggers()
|
|
101
|
+
|
|
102
|
+
# Check if logger already exists in the registry
|
|
103
|
+
if name in cls._logger_registry:
|
|
104
|
+
logger, _ = cls._logger_registry[name]
|
|
105
|
+
# Update timestamp for LRU tracking
|
|
106
|
+
cls._logger_registry[name] = (logger, time.time())
|
|
107
|
+
return logger
|
|
108
|
+
|
|
109
|
+
# Ensure registry size limit
|
|
110
|
+
cls._enforce_size_limit()
|
|
111
|
+
|
|
112
|
+
# Create a new logger and cache it with timestamp
|
|
113
|
+
logger = cls.create_logger(logger_type, name=name, **kwargs)
|
|
114
|
+
cls._logger_registry[name] = (logger, time.time())
|
|
115
|
+
return logger
|
|
116
|
+
|
|
117
|
+
@classmethod
|
|
118
|
+
def clear_registry(cls) -> None:
|
|
119
|
+
"""Clear the logger registry with proper resource cleanup."""
|
|
120
|
+
with cls._registry_lock:
|
|
121
|
+
for logger, _ in cls._logger_registry.values():
|
|
122
|
+
cls._cleanup_logger(logger)
|
|
123
|
+
cls._logger_registry.clear()
|
|
124
|
+
|
|
125
|
+
@classmethod
|
|
126
|
+
def _cleanup_expired_loggers(cls) -> None:
|
|
127
|
+
"""Remove expired loggers from registry based on TTL."""
|
|
128
|
+
current_time = time.time()
|
|
129
|
+
expired_keys = []
|
|
130
|
+
|
|
131
|
+
for name, (logger, timestamp) in cls._logger_registry.items():
|
|
132
|
+
if current_time - timestamp > cls._logger_ttl:
|
|
133
|
+
expired_keys.append(name)
|
|
134
|
+
cls._cleanup_logger(logger)
|
|
135
|
+
|
|
136
|
+
for key in expired_keys:
|
|
137
|
+
cls._logger_registry.pop(key, None)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def _enforce_size_limit(cls) -> None:
|
|
141
|
+
"""Enforce maximum registry size by removing the oldest entries (LRU eviction)."""
|
|
142
|
+
if cls._max_loggers <= 0:
|
|
143
|
+
# Special case: if max_loggers is 0 or negative, clear all
|
|
144
|
+
cls.clear_registry()
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
if len(cls._logger_registry) >= cls._max_loggers:
|
|
148
|
+
# Sort by timestamp (oldest first) and remove the oldest entries
|
|
149
|
+
sorted_entries = sorted(cls._logger_registry.items(), key=lambda x: x[1][1])
|
|
150
|
+
entries_to_remove = len(sorted_entries) - cls._max_loggers + 1
|
|
151
|
+
|
|
152
|
+
for i in range(min(entries_to_remove, len(sorted_entries))):
|
|
153
|
+
name, (logger, _) = sorted_entries[i]
|
|
154
|
+
cls._cleanup_logger(logger)
|
|
155
|
+
cls._logger_registry.pop(name, None)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def set_memory_limits(cls, max_loggers: int = 100, ttl_seconds: int = 3600) -> None:
|
|
159
|
+
"""Configure memory management limits for the logger registry at runtime.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
max_loggers: Maximum number of cached loggers
|
|
163
|
+
ttl_seconds: Time-to-live for cached loggers in seconds
|
|
164
|
+
"""
|
|
165
|
+
with cls._registry_lock:
|
|
166
|
+
cls._max_loggers = max_loggers
|
|
167
|
+
cls._logger_ttl = ttl_seconds
|
|
168
|
+
cls._initialized = True # Mark as manually configured
|
|
169
|
+
# Clean up immediately with new settings
|
|
170
|
+
cls._cleanup_expired_loggers()
|
|
171
|
+
cls._enforce_size_limit()
|
|
172
|
+
|
|
173
|
+
@classmethod
|
|
174
|
+
def _atexit_cleanup(cls) -> None:
|
|
175
|
+
"""Cleanup function registered with atexit to ensure proper resource cleanup."""
|
|
176
|
+
try:
|
|
177
|
+
cls.clear_registry()
|
|
178
|
+
except Exception:
|
|
179
|
+
# Silently ignore exceptions during shutdown cleanup
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
@staticmethod
|
|
183
|
+
def _cleanup_logger(logger: logging.Logger) -> None:
|
|
184
|
+
"""Clean up logger resources by closing all handlers."""
|
|
185
|
+
cleanup_logger_handlers(logger)
|
|
186
|
+
|
|
187
|
+
@classmethod
|
|
188
|
+
def shutdown_logger(cls, name: str) -> bool:
|
|
189
|
+
"""Shutdown and remove a specific logger from registry.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
name: Logger name to shut down
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if logger was found and shutdown, False otherwise
|
|
196
|
+
"""
|
|
197
|
+
with cls._registry_lock:
|
|
198
|
+
if name in cls._logger_registry:
|
|
199
|
+
logger, _ = cls._logger_registry.pop(name)
|
|
200
|
+
cls._cleanup_logger(logger)
|
|
201
|
+
return True
|
|
202
|
+
return False
|
|
203
|
+
|
|
204
|
+
@classmethod
|
|
205
|
+
def get_registered_loggers(cls) -> dict[str, logging.Logger]:
|
|
206
|
+
"""Get all registered loggers. Returns a copy of the registry."""
|
|
207
|
+
with cls._registry_lock:
|
|
208
|
+
return {name: logger for name, (logger, _) in cls._logger_registry.items()}
|
|
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
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def create_logger(
|
|
225
|
+
logger_type: Union[LoggerType, str], config: Optional[LoggerConfig] = None, **kwargs
|
|
226
|
+
) -> logging.Logger:
|
|
227
|
+
"""
|
|
228
|
+
Factory method to create loggers based on type.
|
|
229
|
+
|
|
230
|
+
Args:
|
|
231
|
+
logger_type: Type of logger to create (LoggerType enum or string)
|
|
232
|
+
config: LoggerConfig object with logger parameters
|
|
233
|
+
**kwargs: Individual logger parameters (for backward compatibility)
|
|
234
|
+
|
|
235
|
+
Returns:
|
|
236
|
+
Configured logger instance
|
|
237
|
+
|
|
238
|
+
Raises:
|
|
239
|
+
ValueError: If invalid logger_type is provided
|
|
240
|
+
"""
|
|
241
|
+
# Convert string to enum if needed
|
|
242
|
+
if isinstance(logger_type, str):
|
|
243
|
+
try:
|
|
244
|
+
logger_type = LoggerType(logger_type.lower())
|
|
245
|
+
except ValueError:
|
|
246
|
+
raise ValueError(f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}")
|
|
247
|
+
|
|
248
|
+
# Merge config and kwargs (kwargs take precedence for backward compatibility)
|
|
249
|
+
if config is None:
|
|
250
|
+
config = LoggerConfig()
|
|
251
|
+
|
|
252
|
+
# Create a new config with kwargs overriding config values
|
|
253
|
+
final_config = LoggerConfig(
|
|
254
|
+
level=kwargs.get('level', config.level),
|
|
255
|
+
name=kwargs.get('name', config.name),
|
|
256
|
+
directory=kwargs.get('directory', config.directory),
|
|
257
|
+
filenames=kwargs.get('filenames', config.filenames),
|
|
258
|
+
encoding=kwargs.get('encoding', config.encoding),
|
|
259
|
+
datefmt=kwargs.get('datefmt', config.datefmt),
|
|
260
|
+
timezone=kwargs.get('timezone', config.timezone),
|
|
261
|
+
streamhandler=kwargs.get('streamhandler', config.streamhandler),
|
|
262
|
+
showlocation=kwargs.get('showlocation', config.showlocation),
|
|
263
|
+
maxmbytes=kwargs.get('maxmbytes', config.maxmbytes),
|
|
264
|
+
when=kwargs.get('when', config.when),
|
|
265
|
+
sufix=kwargs.get('sufix', config.sufix),
|
|
266
|
+
rotateatutc=kwargs.get('rotateatutc', config.rotateatutc),
|
|
267
|
+
daystokeep=kwargs.get('daystokeep', config.daystokeep),
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
# Convert enum values to strings for logger classes
|
|
271
|
+
level_str = final_config.level.value if isinstance(final_config.level, LogLevel) else final_config.level
|
|
272
|
+
when_str = final_config.when.value if isinstance(final_config.when, RotateWhen) else final_config.when
|
|
273
|
+
|
|
274
|
+
# Create logger based on type
|
|
275
|
+
match logger_type:
|
|
276
|
+
case LoggerType.BASIC:
|
|
277
|
+
logger_instance = BasicLog(
|
|
278
|
+
level=level_str,
|
|
279
|
+
name=final_config.name,
|
|
280
|
+
encoding=final_config.encoding,
|
|
281
|
+
datefmt=final_config.datefmt,
|
|
282
|
+
timezone=final_config.timezone,
|
|
283
|
+
showlocation=final_config.showlocation,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
case LoggerType.SIZE_ROTATING:
|
|
287
|
+
logger_instance = SizeRotatingLog(
|
|
288
|
+
level=level_str,
|
|
289
|
+
name=final_config.name,
|
|
290
|
+
directory=final_config.directory,
|
|
291
|
+
filenames=final_config.filenames,
|
|
292
|
+
maxmbytes=final_config.maxmbytes,
|
|
293
|
+
daystokeep=final_config.daystokeep,
|
|
294
|
+
encoding=final_config.encoding,
|
|
295
|
+
datefmt=final_config.datefmt,
|
|
296
|
+
timezone=final_config.timezone,
|
|
297
|
+
streamhandler=final_config.streamhandler,
|
|
298
|
+
showlocation=final_config.showlocation,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
case LoggerType.TIMED_ROTATING:
|
|
302
|
+
logger_instance = TimedRotatingLog(
|
|
303
|
+
level=level_str,
|
|
304
|
+
name=final_config.name,
|
|
305
|
+
directory=final_config.directory,
|
|
306
|
+
filenames=final_config.filenames,
|
|
307
|
+
when=when_str,
|
|
308
|
+
sufix=final_config.sufix,
|
|
309
|
+
daystokeep=final_config.daystokeep,
|
|
310
|
+
encoding=final_config.encoding,
|
|
311
|
+
datefmt=final_config.datefmt,
|
|
312
|
+
timezone=final_config.timezone,
|
|
313
|
+
streamhandler=final_config.streamhandler,
|
|
314
|
+
showlocation=final_config.showlocation,
|
|
315
|
+
rotateatutc=final_config.rotateatutc,
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
case _:
|
|
319
|
+
raise ValueError(f"Unsupported logger type: {logger_type}")
|
|
320
|
+
|
|
321
|
+
return logger_instance.init()
|
|
322
|
+
|
|
323
|
+
@staticmethod
|
|
324
|
+
def create_basic_logger(
|
|
325
|
+
level: Optional[Union[LogLevel, str]] = None,
|
|
326
|
+
name: Optional[str] = None,
|
|
327
|
+
encoding: Optional[str] = None,
|
|
328
|
+
datefmt: Optional[str] = None,
|
|
329
|
+
timezone: Optional[str] = None,
|
|
330
|
+
showlocation: Optional[bool] = None,
|
|
331
|
+
) -> logging.Logger:
|
|
332
|
+
"""Convenience method for creating a basic logger"""
|
|
333
|
+
return LoggerFactory.create_logger(
|
|
334
|
+
LoggerType.BASIC,
|
|
335
|
+
level=level,
|
|
336
|
+
name=name,
|
|
337
|
+
encoding=encoding,
|
|
338
|
+
datefmt=datefmt,
|
|
339
|
+
timezone=timezone,
|
|
340
|
+
showlocation=showlocation,
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
@staticmethod
|
|
344
|
+
def create_size_rotating_logger(
|
|
345
|
+
level: Optional[Union[LogLevel, str]] = None,
|
|
346
|
+
name: Optional[str] = None,
|
|
347
|
+
directory: Optional[str] = None,
|
|
348
|
+
filenames: Optional[list | tuple] = None,
|
|
349
|
+
maxmbytes: Optional[int] = None,
|
|
350
|
+
daystokeep: Optional[int] = None,
|
|
351
|
+
encoding: Optional[str] = None,
|
|
352
|
+
datefmt: Optional[str] = None,
|
|
353
|
+
timezone: Optional[str] = None,
|
|
354
|
+
streamhandler: Optional[bool] = None,
|
|
355
|
+
showlocation: Optional[bool] = None,
|
|
356
|
+
) -> logging.Logger:
|
|
357
|
+
"""Convenience method for creating a size rotating logger"""
|
|
358
|
+
return LoggerFactory.create_logger(
|
|
359
|
+
LoggerType.SIZE_ROTATING,
|
|
360
|
+
level=level,
|
|
361
|
+
name=name,
|
|
362
|
+
directory=directory,
|
|
363
|
+
filenames=filenames,
|
|
364
|
+
maxmbytes=maxmbytes,
|
|
365
|
+
daystokeep=daystokeep,
|
|
366
|
+
encoding=encoding,
|
|
367
|
+
datefmt=datefmt,
|
|
368
|
+
timezone=timezone,
|
|
369
|
+
streamhandler=streamhandler,
|
|
370
|
+
showlocation=showlocation,
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
@staticmethod
|
|
374
|
+
def create_timed_rotating_logger(
|
|
375
|
+
level: Optional[Union[LogLevel, str]] = None,
|
|
376
|
+
name: Optional[str] = None,
|
|
377
|
+
directory: Optional[str] = None,
|
|
378
|
+
filenames: Optional[list | tuple] = None,
|
|
379
|
+
when: Optional[Union[RotateWhen, str]] = None,
|
|
380
|
+
sufix: Optional[str] = None,
|
|
381
|
+
daystokeep: Optional[int] = None,
|
|
382
|
+
encoding: Optional[str] = None,
|
|
383
|
+
datefmt: Optional[str] = None,
|
|
384
|
+
timezone: Optional[str] = None,
|
|
385
|
+
streamhandler: Optional[bool] = None,
|
|
386
|
+
showlocation: Optional[bool] = None,
|
|
387
|
+
rotateatutc: Optional[bool] = None,
|
|
388
|
+
) -> logging.Logger:
|
|
389
|
+
"""Convenience method for creating a timed rotating logger"""
|
|
390
|
+
return LoggerFactory.create_logger(
|
|
391
|
+
LoggerType.TIMED_ROTATING,
|
|
392
|
+
level=level,
|
|
393
|
+
name=name,
|
|
394
|
+
directory=directory,
|
|
395
|
+
filenames=filenames,
|
|
396
|
+
when=when,
|
|
397
|
+
sufix=sufix,
|
|
398
|
+
daystokeep=daystokeep,
|
|
399
|
+
encoding=encoding,
|
|
400
|
+
datefmt=datefmt,
|
|
401
|
+
timezone=timezone,
|
|
402
|
+
streamhandler=streamhandler,
|
|
403
|
+
showlocation=showlocation,
|
|
404
|
+
rotateatutc=rotateatutc,
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# Convenience functions for backward compatibility and easier usage
|
|
409
|
+
def create_logger(logger_type: Union[LoggerType, str], **kwargs) -> logging.Logger:
|
|
410
|
+
"""Convenience function to create a logger using the factory"""
|
|
411
|
+
return LoggerFactory.create_logger(logger_type, **kwargs)
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def get_or_create_logger(logger_type: Union[LoggerType, str], **kwargs) -> logging.Logger:
|
|
415
|
+
"""Convenience function to get cached or create a logger using the factory"""
|
|
416
|
+
return LoggerFactory.get_or_create_logger(logger_type, **kwargs)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def basic_logger(**kwargs) -> logging.Logger:
|
|
420
|
+
"""Convenience function to create a basic logger"""
|
|
421
|
+
return LoggerFactory.create_basic_logger(**kwargs)
|
|
422
|
+
|
|
423
|
+
|
|
424
|
+
def size_rotating_logger(**kwargs) -> logging.Logger:
|
|
425
|
+
"""Convenience function to create a size rotating logger"""
|
|
426
|
+
return LoggerFactory.create_size_rotating_logger(**kwargs)
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def timed_rotating_logger(**kwargs) -> logging.Logger:
|
|
430
|
+
"""Convenience function to create a timed rotating logger"""
|
|
431
|
+
return LoggerFactory.create_timed_rotating_logger(**kwargs)
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def clear_logger_registry() -> None:
|
|
435
|
+
"""Convenience function to clear the logger registry with proper cleanup"""
|
|
436
|
+
LoggerFactory.clear_registry()
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def shutdown_logger(name: str) -> bool:
|
|
440
|
+
"""Convenience function to shut down a specific logger"""
|
|
441
|
+
return LoggerFactory.shutdown_logger(name)
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def get_registered_loggers() -> dict[str, logging.Logger]:
|
|
445
|
+
"""Convenience function to get all registered loggers"""
|
|
446
|
+
return LoggerFactory.get_registered_loggers()
|