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.
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pythonLogs
3
- Version: 5.0.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 (>=2.10.1,<3.0.0)
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.0.0,<3.0.0"]
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.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 = "^2.10.1"
42
+ pydantic-settings = "2.11.0"
43
43
  python-dotenv = "^1.1.1"
44
44
 
45
45
  [tool.poetry.group.test.dependencies]
46
- coverage = "^7.9.2"
47
- poethepoet = "^0.36.0"
48
- psutil = "^7.0.0"
49
- pytest = "^8.4.1"
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 = "coverage run -m pytest -v"
53
- _coverage_report = "coverage report"
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.memory_utils import cleanup_logger_handlers, register_logger_weakref
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.memory_utils import cleanup_logger_handlers
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, **kwargs,
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) in (0, 1):
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 _get_stderr_timezone():
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
- return ZoneInfo(timezone_name)
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 = _get_stderr_timezone()
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 _get_timezone_offset(timezone_: str) -> str:
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
- return datetime.now(ZoneInfo(timezone_)).strftime("%z")
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 = _get_timezone_offset(timezone_)
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
- try:
239
- with open(file_path, "rb") as fin:
240
- with gzip.open(renamed_dst, "wb", compresslevel=6) as fout: # Balanced compression
241
- shutil.copyfileobj(fin, fout, length=64*1024) # type: ignore # 64KB chunks for better performance
242
- except (OSError, IOError) as e:
243
- write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
244
- raise e
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 better performance"""
279
+ """Get timezone function with caching and fallback for missing timezone data"""
258
280
  match time_zone.lower():
259
281
  case "utc":
260
- return time.gmtime
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
- # Cache the timezone object
265
- tz = ZoneInfo(time_zone)
266
- return lambda *args: datetime.now(tz=tz).timetuple()
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
- from . import log_utils
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
- from . import log_utils
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
- with factory.LoggerFactory._registry_lock:
147
- registry_size = len(factory.LoggerFactory._logger_registry)
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
- from . import log_utils
153
- with log_utils._directory_lock:
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': directory_cache_size,
134
+ 'directory_cache_size': directory_stats['cached_directories'],
160
135
  'active_logger_count': get_active_logger_count(),
161
- 'max_registry_size': factory.LoggerFactory._max_loggers,
136
+ 'max_registry_size': factory_limits['max_loggers'],
162
137
  'max_formatter_cache': _max_formatters,
163
- 'max_directory_cache': log_utils._max_cached_directories,
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
- from . import log_utils
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._get_timezone_offset.cache_clear()
179
- log_utils._get_timezone_offset = lru_cache(maxsize=8)(log_utils._get_timezone_offset.__wrapped__)
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._get_stderr_timezone.cache_clear()
183
- log_utils._get_stderr_timezone = lru_cache(maxsize=4)(log_utils._get_stderr_timezone.__wrapped__)
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="/app/logs")
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 cleanup_logger_handlers, register_logger_weakref
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 for method_name in namespace
27
- if (callable(getattr(cls, method_name, None)) and
28
- not method_name.startswith('_') and
29
- method_name not in ['__enter__', '__exit__', '__init__'])
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 for method_name in dir(cls)
66
- if (callable(getattr(cls, method_name, None)) and
67
- not method_name.startswith('_') and
68
- method_name not in ['__enter__', '__exit__', '__init__'])
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 (hasattr(cls, method_name) and
81
- callable(original_method) and
82
- not hasattr(original_method, '_thread_safe_wrapped'))
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
- remove_old_logs,
13
+ remove_old_logs,
14
14
  )
15
- from pythonLogs.memory_utils import cleanup_logger_handlers, register_logger_weakref
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
- datefmt: Optional[str] = None,
44
- timezone: Optional[str] = None,
45
- streamhandler: Optional[bool] = None,
46
- showlocation: Optional[bool] = None,
47
- rotateatutc: Optional[bool] = 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
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