pythonLogs 6.0.0__tar.gz → 6.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-6.0.0 → pythonlogs-6.0.2}/PKG-INFO +51 -19
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/README.md +45 -9
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pyproject.toml +45 -32
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/.env.example +1 -0
- pythonlogs-6.0.2/pythonLogs/__init__.py +24 -0
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/basic_log.py +3 -3
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/constants.py +3 -3
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/factory.py +105 -105
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/log_utils.py +15 -14
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/memory_utils.py +20 -19
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/settings.py +26 -5
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/thread_safety.py +24 -23
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/size_rotating.py +2 -2
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/timed_rotating.py +1 -1
- pythonlogs-6.0.0/pythonLogs/__init__.py +0 -63
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/.gitignore +0 -0
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/LICENSE +0 -0
- {pythonlogs-6.0.0 → pythonlogs-6.0.2}/pythonLogs/core/__init__.py +0 -0
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pythonLogs
|
|
3
|
-
Version: 6.0.
|
|
3
|
+
Version: 6.0.2
|
|
4
4
|
Summary: High-performance Python logging library with file rotation and optimized caching for better performance
|
|
5
|
-
Project-URL: Homepage, https://pypi.org/project/pythonLogs
|
|
6
5
|
Project-URL: Repository, https://github.com/ddc/pythonLogs
|
|
7
|
-
|
|
6
|
+
Project-URL: Homepage, https://pypi.org/project/pythonLogs
|
|
7
|
+
Author-email: Daniel Costa <ddcsoftwares@proton.me>
|
|
8
8
|
Maintainer: Daniel Costa
|
|
9
9
|
License: MIT
|
|
10
10
|
License-File: LICENSE
|
|
@@ -16,17 +16,13 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
16
16
|
Classifier: Natural Language :: English
|
|
17
17
|
Classifier: Operating System :: OS Independent
|
|
18
18
|
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
21
22
|
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
-
Requires-Python: >=3.
|
|
24
|
-
Requires-Dist: pydantic-settings>=2.
|
|
25
|
-
Provides-Extra: test
|
|
26
|
-
Requires-Dist: poethepoet>=0.40.0; extra == 'test'
|
|
27
|
-
Requires-Dist: psutil>=7.2.1; extra == 'test'
|
|
28
|
-
Requires-Dist: pytest-cov>=7.0.0; extra == 'test'
|
|
29
|
-
Requires-Dist: pytest>=9.0.2; extra == 'test'
|
|
24
|
+
Requires-Python: >=3.11
|
|
25
|
+
Requires-Dist: pydantic-settings>=2.11.0
|
|
30
26
|
Description-Content-Type: text/markdown
|
|
31
27
|
|
|
32
28
|
<h1 align="center">
|
|
@@ -39,9 +35,9 @@ Description-Content-Type: text/markdown
|
|
|
39
35
|
<a href="https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ"><img src="https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic" alt="Donate"/></a>
|
|
40
36
|
<a href="https://github.com/sponsors/ddc"><img src="https://img.shields.io/static/v1?style=plastic&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4" alt="Sponsor"/></a>
|
|
41
37
|
<br>
|
|
38
|
+
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
|
|
42
39
|
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json?style=plastic" alt="uv"/></a>
|
|
43
40
|
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json?style=plastic" alt="Ruff"/></a>
|
|
44
|
-
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
|
|
45
41
|
<br>
|
|
46
42
|
<a href="https://www.python.org/downloads"><img src="https://img.shields.io/pypi/pyversions/pythonLogs.svg?style=plastic&logo=python&cacheSeconds=3600" alt="Python"/></a>
|
|
47
43
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
|
|
@@ -69,9 +65,12 @@ Description-Content-Type: text/markdown
|
|
|
69
65
|
- [Context Manager Support](#context-manager-support)
|
|
70
66
|
- [Using With Multiple Log Levels and Files](#using-with-multiple-log-levels-and-files)
|
|
71
67
|
- [Environment Variables](#env-variables-optional)
|
|
68
|
+
- [Settings Cache Management](#settings-cache-management)
|
|
72
69
|
- [Flexible Configuration Options](#flexible-configuration-options)
|
|
73
70
|
- [Development](#development)
|
|
74
|
-
- [Create DEV Environment
|
|
71
|
+
- [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
|
|
72
|
+
- [Update DEV Environment Packages](#update-dev-environment-packages)
|
|
73
|
+
- [Building Wheel](#building-wheel)
|
|
75
74
|
- [Optionals](#optionals)
|
|
76
75
|
- [License](#license)
|
|
77
76
|
- [Support](#support)
|
|
@@ -316,6 +315,25 @@ LOG_ROTATE_AT_UTC=True
|
|
|
316
315
|
LOG_ROTATE_FILE_SUFIX="%Y%m%d"
|
|
317
316
|
```
|
|
318
317
|
|
|
318
|
+
## Settings Cache Management
|
|
319
|
+
|
|
320
|
+
Use `get_log_settings()` to inspect current configuration and `clear_settings_cache()` to reload configuration from environment variables:
|
|
321
|
+
|
|
322
|
+
```python
|
|
323
|
+
from pythonLogs import get_log_settings, clear_settings_cache
|
|
324
|
+
|
|
325
|
+
# Inspect current settings
|
|
326
|
+
settings = get_log_settings()
|
|
327
|
+
print(settings.level) # Current log level
|
|
328
|
+
print(settings.timezone) # Current timezone
|
|
329
|
+
|
|
330
|
+
# Clear cache and reload .env on next access (default)
|
|
331
|
+
clear_settings_cache()
|
|
332
|
+
|
|
333
|
+
# Clear cache but keep current .env values
|
|
334
|
+
clear_settings_cache(reload_env=False)
|
|
335
|
+
```
|
|
336
|
+
|
|
319
337
|
|
|
320
338
|
|
|
321
339
|
|
|
@@ -360,20 +378,34 @@ RotateWhen.MONDAY # "W0"
|
|
|
360
378
|
|
|
361
379
|
# Development
|
|
362
380
|
|
|
363
|
-
Must have [UV](https://uv.run/docs/getting-started/installation)
|
|
364
|
-
[Black](https://black.readthedocs.io/en/stable/getting_started.html),
|
|
365
|
-
[Ruff](https://docs.astral.sh/ruff/installation/), and
|
|
366
|
-
[Poe the Poet](https://poethepoet.naber.dev/installation) installed.
|
|
381
|
+
Must have [UV](https://uv.run/docs/getting-started/installation) installed.
|
|
367
382
|
|
|
368
|
-
## Create DEV Environment
|
|
383
|
+
## Create DEV Environment and Running Tests
|
|
384
|
+
|
|
385
|
+
> **Note:** All poe tasks automatically run ruff linter along with Black formatting
|
|
369
386
|
|
|
370
387
|
```shell
|
|
371
|
-
uv sync --all-extras
|
|
372
|
-
poe linter
|
|
388
|
+
uv sync --all-extras --all-groups
|
|
373
389
|
poe test
|
|
390
|
+
```
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
## Update DEV Environment Packages
|
|
394
|
+
This will update all packages dependencies
|
|
395
|
+
|
|
396
|
+
```shell
|
|
397
|
+
poe updatedev
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
## Building Wheel
|
|
402
|
+
This will update all packages, run linter, both unit and integration tests and finally build the wheel
|
|
403
|
+
|
|
404
|
+
```shell
|
|
374
405
|
poe build
|
|
375
406
|
```
|
|
376
407
|
|
|
408
|
+
|
|
377
409
|
## Optionals
|
|
378
410
|
|
|
379
411
|
### Create a cprofile.prof file from unit tests
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
<a href="https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ"><img src="https://img.shields.io/badge/Donate-PayPal-brightgreen.svg?style=plastic" alt="Donate"/></a>
|
|
9
9
|
<a href="https://github.com/sponsors/ddc"><img src="https://img.shields.io/static/v1?style=plastic&label=Sponsor&message=%E2%9D%A4&logo=GitHub&color=ff69b4" alt="Sponsor"/></a>
|
|
10
10
|
<br>
|
|
11
|
+
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
|
|
11
12
|
<a href="https://github.com/astral-sh/uv"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json?style=plastic" alt="uv"/></a>
|
|
12
13
|
<a href="https://github.com/astral-sh/ruff"><img src="https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json?style=plastic" alt="Ruff"/></a>
|
|
13
|
-
<a href="https://github.com/psf/black"><img src="https://img.shields.io/badge/code%20style-black-000000.svg?style=plastic" alt="Code style: black"/></a>
|
|
14
14
|
<br>
|
|
15
15
|
<a href="https://www.python.org/downloads"><img src="https://img.shields.io/pypi/pyversions/pythonLogs.svg?style=plastic&logo=python&cacheSeconds=3600" alt="Python"/></a>
|
|
16
16
|
<a href="https://opensource.org/licenses/MIT"><img src="https://img.shields.io/badge/License-MIT-yellow.svg?style=plastic" alt="License: MIT"/></a>
|
|
@@ -38,9 +38,12 @@
|
|
|
38
38
|
- [Context Manager Support](#context-manager-support)
|
|
39
39
|
- [Using With Multiple Log Levels and Files](#using-with-multiple-log-levels-and-files)
|
|
40
40
|
- [Environment Variables](#env-variables-optional)
|
|
41
|
+
- [Settings Cache Management](#settings-cache-management)
|
|
41
42
|
- [Flexible Configuration Options](#flexible-configuration-options)
|
|
42
43
|
- [Development](#development)
|
|
43
|
-
- [Create DEV Environment
|
|
44
|
+
- [Create DEV Environment and Running Tests](#create-dev-environment-and-running-tests)
|
|
45
|
+
- [Update DEV Environment Packages](#update-dev-environment-packages)
|
|
46
|
+
- [Building Wheel](#building-wheel)
|
|
44
47
|
- [Optionals](#optionals)
|
|
45
48
|
- [License](#license)
|
|
46
49
|
- [Support](#support)
|
|
@@ -285,6 +288,25 @@ LOG_ROTATE_AT_UTC=True
|
|
|
285
288
|
LOG_ROTATE_FILE_SUFIX="%Y%m%d"
|
|
286
289
|
```
|
|
287
290
|
|
|
291
|
+
## Settings Cache Management
|
|
292
|
+
|
|
293
|
+
Use `get_log_settings()` to inspect current configuration and `clear_settings_cache()` to reload configuration from environment variables:
|
|
294
|
+
|
|
295
|
+
```python
|
|
296
|
+
from pythonLogs import get_log_settings, clear_settings_cache
|
|
297
|
+
|
|
298
|
+
# Inspect current settings
|
|
299
|
+
settings = get_log_settings()
|
|
300
|
+
print(settings.level) # Current log level
|
|
301
|
+
print(settings.timezone) # Current timezone
|
|
302
|
+
|
|
303
|
+
# Clear cache and reload .env on next access (default)
|
|
304
|
+
clear_settings_cache()
|
|
305
|
+
|
|
306
|
+
# Clear cache but keep current .env values
|
|
307
|
+
clear_settings_cache(reload_env=False)
|
|
308
|
+
```
|
|
309
|
+
|
|
288
310
|
|
|
289
311
|
|
|
290
312
|
|
|
@@ -329,20 +351,34 @@ RotateWhen.MONDAY # "W0"
|
|
|
329
351
|
|
|
330
352
|
# Development
|
|
331
353
|
|
|
332
|
-
Must have [UV](https://uv.run/docs/getting-started/installation)
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
[Poe the Poet](https://poethepoet.naber.dev/installation) installed.
|
|
354
|
+
Must have [UV](https://uv.run/docs/getting-started/installation) installed.
|
|
355
|
+
|
|
356
|
+
## Create DEV Environment and Running Tests
|
|
336
357
|
|
|
337
|
-
|
|
358
|
+
> **Note:** All poe tasks automatically run ruff linter along with Black formatting
|
|
338
359
|
|
|
339
360
|
```shell
|
|
340
|
-
uv sync --all-extras
|
|
341
|
-
poe linter
|
|
361
|
+
uv sync --all-extras --all-groups
|
|
342
362
|
poe test
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
## Update DEV Environment Packages
|
|
367
|
+
This will update all packages dependencies
|
|
368
|
+
|
|
369
|
+
```shell
|
|
370
|
+
poe updatedev
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
## Building Wheel
|
|
375
|
+
This will update all packages, run linter, both unit and integration tests and finally build the wheel
|
|
376
|
+
|
|
377
|
+
```shell
|
|
343
378
|
poe build
|
|
344
379
|
```
|
|
345
380
|
|
|
381
|
+
|
|
346
382
|
## Optionals
|
|
347
383
|
|
|
348
384
|
### Create a cprofile.prof file from unit tests
|
|
@@ -2,16 +2,31 @@
|
|
|
2
2
|
requires = ["hatchling"]
|
|
3
3
|
build-backend = "hatchling.build"
|
|
4
4
|
|
|
5
|
+
[tool.hatch.metadata]
|
|
6
|
+
allow-direct-references = true
|
|
7
|
+
|
|
8
|
+
[tool.hatch.build]
|
|
9
|
+
include = ["pythonLogs/**/*"]
|
|
10
|
+
|
|
11
|
+
[tool.hatch.build.targets.wheel]
|
|
12
|
+
packages = ["pythonLogs"]
|
|
13
|
+
|
|
5
14
|
[project]
|
|
6
15
|
name = "pythonLogs"
|
|
7
|
-
version = "6.0.
|
|
16
|
+
version = "6.0.2"
|
|
8
17
|
description = "High-performance Python logging library with file rotation and optimized caching for better performance"
|
|
18
|
+
urls.Repository = "https://github.com/ddc/pythonLogs"
|
|
19
|
+
urls.Homepage = "https://pypi.org/project/pythonLogs"
|
|
9
20
|
license = {text = "MIT"}
|
|
10
21
|
readme = "README.md"
|
|
11
|
-
authors = [
|
|
12
|
-
|
|
22
|
+
authors = [
|
|
23
|
+
{name = "Daniel Costa", email = "ddcsoftwares@proton.me"},
|
|
24
|
+
]
|
|
25
|
+
maintainers = [
|
|
26
|
+
{name = "Daniel Costa"},
|
|
27
|
+
]
|
|
13
28
|
keywords = [
|
|
14
|
-
"
|
|
29
|
+
"python", "python3", "python-3",
|
|
15
30
|
"log", "logging", "logger",
|
|
16
31
|
"logutils", "log-utils", "pythonLogs"
|
|
17
32
|
]
|
|
@@ -20,6 +35,7 @@ classifiers = [
|
|
|
20
35
|
"Development Status :: 5 - Production/Stable",
|
|
21
36
|
"License :: OSI Approved :: MIT License",
|
|
22
37
|
"Programming Language :: Python :: 3",
|
|
38
|
+
"Programming Language :: Python :: 3.11",
|
|
23
39
|
"Programming Language :: Python :: 3.12",
|
|
24
40
|
"Programming Language :: Python :: 3.13",
|
|
25
41
|
"Programming Language :: Python :: 3.14",
|
|
@@ -28,52 +44,44 @@ classifiers = [
|
|
|
28
44
|
"Intended Audience :: Developers",
|
|
29
45
|
"Natural Language :: English",
|
|
30
46
|
]
|
|
31
|
-
requires-python = ">=3.
|
|
47
|
+
requires-python = ">=3.11"
|
|
32
48
|
dependencies = [
|
|
33
|
-
"pydantic-settings>=2.
|
|
49
|
+
"pydantic-settings>=2.11.0",
|
|
34
50
|
]
|
|
35
51
|
|
|
36
|
-
[
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
[project.optional-dependencies]
|
|
41
|
-
test = [
|
|
42
|
-
"poethepoet>=0.40.0",
|
|
43
|
-
"psutil>=7.2.1",
|
|
44
|
-
"pytest>=9.0.2",
|
|
52
|
+
[dependency-groups]
|
|
53
|
+
dev = [
|
|
54
|
+
"psutil>=7.2.2",
|
|
45
55
|
"pytest-cov>=7.0.0",
|
|
56
|
+
"poethepoet>=0.41.0",
|
|
57
|
+
"ruff>=0.15.0",
|
|
58
|
+
"black>=26.1.0",
|
|
46
59
|
]
|
|
47
60
|
|
|
48
|
-
[tool.hatch.build]
|
|
49
|
-
include = ["pythonLogs/**/*"]
|
|
50
|
-
|
|
51
|
-
[tool.hatch.build.targets.wheel]
|
|
52
|
-
packages = ["pythonLogs"]
|
|
53
|
-
|
|
54
61
|
[tool.poe.tasks]
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
test = "uv
|
|
62
|
+
linter.shell = "uv run ruff check --fix . && uv run black ."
|
|
63
|
+
profile.sequence = ["linter", {shell = "uv run python -m cProfile -o cprofile_unit.prof -m pytest --no-cov"}]
|
|
64
|
+
test.sequence = ["linter", {shell = "uv run pytest"}]
|
|
65
|
+
updatedev.sequence = ["linter", {shell = "uv lock --upgrade && uv sync --all-extras --group dev"}]
|
|
66
|
+
build.sequence = ["updatedev", "test", {shell = "uv build --wheel"}]
|
|
60
67
|
|
|
61
68
|
[tool.pytest.ini_options]
|
|
62
69
|
addopts = "-v --cov --cov-report=term --cov-report=xml --junitxml=junit.xml"
|
|
63
70
|
junit_family = "legacy"
|
|
64
71
|
testpaths = ["tests"]
|
|
65
72
|
markers = [
|
|
66
|
-
"slow: marks tests as slow (deselect with '-m \"not slow\"')"
|
|
73
|
+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
|
|
67
74
|
]
|
|
68
75
|
|
|
69
76
|
[tool.coverage.run]
|
|
70
77
|
omit = [
|
|
71
78
|
"tests/*",
|
|
72
79
|
"*/__init__.py",
|
|
73
|
-
"*/_version.py",
|
|
74
80
|
]
|
|
75
81
|
|
|
76
82
|
[tool.coverage.report]
|
|
83
|
+
show_missing = true
|
|
84
|
+
skip_covered = false
|
|
77
85
|
exclude_lines = [
|
|
78
86
|
"pragma: no cover",
|
|
79
87
|
"def __repr__",
|
|
@@ -86,8 +94,6 @@ exclude_lines = [
|
|
|
86
94
|
"class .*\\bProtocol\\):",
|
|
87
95
|
"@(abc\\.)?abstractmethod",
|
|
88
96
|
]
|
|
89
|
-
show_missing = true
|
|
90
|
-
skip_covered = false
|
|
91
97
|
|
|
92
98
|
[tool.black]
|
|
93
99
|
line-length = 120
|
|
@@ -95,11 +101,18 @@ skip-string-normalization = true
|
|
|
95
101
|
|
|
96
102
|
[tool.ruff]
|
|
97
103
|
line-length = 120
|
|
104
|
+
target-version = "py311"
|
|
98
105
|
|
|
99
106
|
[tool.ruff.lint]
|
|
100
|
-
select = ["I"]
|
|
107
|
+
select = ["E", "W", "F", "I", "B", "C4", "UP"]
|
|
108
|
+
ignore = ["E501", "E402", "UP046", "UP047"]
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.per-file-ignores]
|
|
111
|
+
"__init__.py" = ["F401"]
|
|
112
|
+
"tests/**/*.py" = ["S101", "S105", "S106", "S311", "SLF001", "F841"]
|
|
101
113
|
|
|
102
114
|
[tool.ruff.lint.isort]
|
|
103
115
|
known-first-party = ["pythonLogs"]
|
|
104
|
-
force-sort-within-sections =
|
|
116
|
+
force-sort-within-sections = false
|
|
117
|
+
from-first = false
|
|
105
118
|
no-sections = true
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from pythonLogs.core.constants import LogLevel, RotateWhen
|
|
4
|
+
from pythonLogs.core.factory import BasicLog, SizeRotatingLog, TimedRotatingLog
|
|
5
|
+
from pythonLogs.core.settings import clear_settings_cache, get_log_settings
|
|
6
|
+
|
|
7
|
+
__all__ = (
|
|
8
|
+
"BasicLog",
|
|
9
|
+
"SizeRotatingLog",
|
|
10
|
+
"TimedRotatingLog",
|
|
11
|
+
"LogLevel",
|
|
12
|
+
"RotateWhen",
|
|
13
|
+
"clear_settings_cache",
|
|
14
|
+
"get_log_settings",
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
__title__ = "pythonLogs"
|
|
18
|
+
__author__ = "Daniel Costa"
|
|
19
|
+
__email__ = "ddcsoftwares@proton.me"
|
|
20
|
+
__license__ = "MIT"
|
|
21
|
+
__copyright__ = "Copyright 2024-present DDC Softwares"
|
|
22
|
+
__version__ = version(__title__)
|
|
23
|
+
|
|
24
|
+
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
@@ -5,7 +5,7 @@ from pythonLogs.core.settings import get_log_settings
|
|
|
5
5
|
from pythonLogs.core.thread_safety import auto_thread_safe
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
@auto_thread_safe([
|
|
8
|
+
@auto_thread_safe(["init"])
|
|
9
9
|
class BasicLog:
|
|
10
10
|
"""Basic logger with context manager support for automatic resource cleanup."""
|
|
11
11
|
|
|
@@ -48,13 +48,13 @@ class BasicLog:
|
|
|
48
48
|
|
|
49
49
|
def __enter__(self):
|
|
50
50
|
"""Context manager entry."""
|
|
51
|
-
if not hasattr(self,
|
|
51
|
+
if not hasattr(self, "logger") or self.logger is None:
|
|
52
52
|
self.init()
|
|
53
53
|
return self.logger
|
|
54
54
|
|
|
55
55
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
56
56
|
"""Context manager exit with automatic cleanup."""
|
|
57
|
-
if hasattr(self,
|
|
57
|
+
if hasattr(self, "logger"):
|
|
58
58
|
cleanup_logger_handlers(self.logger)
|
|
59
59
|
|
|
60
60
|
@staticmethod
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
from enum import Enum
|
|
2
1
|
import logging
|
|
2
|
+
from enum import StrEnum
|
|
3
3
|
|
|
4
4
|
# File and Directory Constants
|
|
5
5
|
MB_TO_BYTES = 1024 * 1024
|
|
@@ -17,7 +17,7 @@ DEFAULT_ENCODING = "UTF-8"
|
|
|
17
17
|
DEFAULT_TIMEZONE = "UTC"
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class LogLevel(
|
|
20
|
+
class LogLevel(StrEnum):
|
|
21
21
|
"""Log levels"""
|
|
22
22
|
|
|
23
23
|
CRITICAL = "CRITICAL"
|
|
@@ -29,7 +29,7 @@ class LogLevel(str, Enum):
|
|
|
29
29
|
DEBUG = "DEBUG"
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
class RotateWhen(
|
|
32
|
+
class RotateWhen(StrEnum):
|
|
33
33
|
"""Rotation timing options for TimedRotatingLog"""
|
|
34
34
|
|
|
35
35
|
MIDNIGHT = "midnight"
|
|
@@ -1,39 +1,39 @@
|
|
|
1
1
|
import atexit
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
from enum import Enum
|
|
4
2
|
import logging
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import StrEnum
|
|
5
7
|
from pythonLogs.basic_log import BasicLog as _BasicLogImpl
|
|
6
8
|
from pythonLogs.core.constants import LogLevel, RotateWhen
|
|
7
9
|
from pythonLogs.core.log_utils import cleanup_logger_handlers
|
|
8
10
|
from pythonLogs.core.settings import get_log_settings
|
|
9
11
|
from pythonLogs.size_rotating import SizeRotatingLog as _SizeRotatingLogImpl
|
|
10
12
|
from pythonLogs.timed_rotating import TimedRotatingLog as _TimedRotatingLogImpl
|
|
11
|
-
import
|
|
12
|
-
import time
|
|
13
|
-
from typing import Dict, Optional, Tuple, Union, assert_never
|
|
13
|
+
from typing import assert_never
|
|
14
14
|
|
|
15
15
|
|
|
16
16
|
@dataclass
|
|
17
17
|
class LoggerConfig:
|
|
18
18
|
"""Configuration class to group logger parameters"""
|
|
19
19
|
|
|
20
|
-
level:
|
|
21
|
-
name:
|
|
22
|
-
directory:
|
|
23
|
-
filenames:
|
|
24
|
-
encoding:
|
|
25
|
-
datefmt:
|
|
26
|
-
timezone:
|
|
27
|
-
streamhandler:
|
|
28
|
-
showlocation:
|
|
29
|
-
maxmbytes:
|
|
30
|
-
when:
|
|
31
|
-
sufix:
|
|
32
|
-
rotateatutc:
|
|
33
|
-
daystokeep:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class LoggerType(
|
|
20
|
+
level: LogLevel | str | None = None
|
|
21
|
+
name: str | None = None
|
|
22
|
+
directory: str | None = None
|
|
23
|
+
filenames: list | tuple | None = None
|
|
24
|
+
encoding: str | None = None
|
|
25
|
+
datefmt: str | None = None
|
|
26
|
+
timezone: str | None = None
|
|
27
|
+
streamhandler: bool | None = None
|
|
28
|
+
showlocation: bool | None = None
|
|
29
|
+
maxmbytes: int | None = None
|
|
30
|
+
when: RotateWhen | str | None = None
|
|
31
|
+
sufix: str | None = None
|
|
32
|
+
rotateatutc: bool | None = None
|
|
33
|
+
daystokeep: int | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class LoggerType(StrEnum):
|
|
37
37
|
"""Available logger types"""
|
|
38
38
|
|
|
39
39
|
BASIC = "basic"
|
|
@@ -45,7 +45,7 @@ class LoggerFactory:
|
|
|
45
45
|
"""Factory for creating different types of loggers with optimized instantiation and memory management"""
|
|
46
46
|
|
|
47
47
|
# Logger registry for reusing loggers by name with timestamp tracking
|
|
48
|
-
_logger_registry:
|
|
48
|
+
_logger_registry: dict[str, tuple[logging.Logger, float]] = {}
|
|
49
49
|
# Thread lock for registry access
|
|
50
50
|
_registry_lock = threading.RLock()
|
|
51
51
|
# Memory optimization settings
|
|
@@ -71,8 +71,8 @@ class LoggerFactory:
|
|
|
71
71
|
@classmethod
|
|
72
72
|
def get_or_create_logger(
|
|
73
73
|
cls,
|
|
74
|
-
logger_type:
|
|
75
|
-
name:
|
|
74
|
+
logger_type: LoggerType | str,
|
|
75
|
+
name: str | None = None,
|
|
76
76
|
**kwargs,
|
|
77
77
|
) -> logging.Logger:
|
|
78
78
|
"""
|
|
@@ -215,12 +215,10 @@ class LoggerFactory:
|
|
|
215
215
|
Dictionary with current max_loggers and ttl_seconds settings
|
|
216
216
|
"""
|
|
217
217
|
with cls._registry_lock:
|
|
218
|
-
return {
|
|
218
|
+
return {"max_loggers": cls._max_loggers, "ttl_seconds": cls._logger_ttl}
|
|
219
219
|
|
|
220
220
|
@staticmethod
|
|
221
|
-
def create_logger(
|
|
222
|
-
logger_type: Union[LoggerType, str], config: Optional[LoggerConfig] = None, **kwargs
|
|
223
|
-
) -> logging.Logger:
|
|
221
|
+
def create_logger(logger_type: LoggerType | str, config: LoggerConfig | None = None, **kwargs) -> logging.Logger:
|
|
224
222
|
"""
|
|
225
223
|
Factory method to create loggers based on type.
|
|
226
224
|
|
|
@@ -239,8 +237,10 @@ class LoggerFactory:
|
|
|
239
237
|
if isinstance(logger_type, str):
|
|
240
238
|
try:
|
|
241
239
|
logger_type = LoggerType(logger_type.lower())
|
|
242
|
-
except ValueError:
|
|
243
|
-
raise ValueError(
|
|
240
|
+
except ValueError as err:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"Invalid logger type: {logger_type}. Valid types: {[t.value for t in LoggerType]}"
|
|
243
|
+
) from err
|
|
244
244
|
|
|
245
245
|
# Merge config and kwargs (kwargs take precedence for backward compatibility)
|
|
246
246
|
if config is None:
|
|
@@ -248,20 +248,20 @@ class LoggerFactory:
|
|
|
248
248
|
|
|
249
249
|
# Create a new config with kwargs overriding config values
|
|
250
250
|
final_config = LoggerConfig(
|
|
251
|
-
level=kwargs.get(
|
|
252
|
-
name=kwargs.get(
|
|
253
|
-
directory=kwargs.get(
|
|
254
|
-
filenames=kwargs.get(
|
|
255
|
-
encoding=kwargs.get(
|
|
256
|
-
datefmt=kwargs.get(
|
|
257
|
-
timezone=kwargs.get(
|
|
258
|
-
streamhandler=kwargs.get(
|
|
259
|
-
showlocation=kwargs.get(
|
|
260
|
-
maxmbytes=kwargs.get(
|
|
261
|
-
when=kwargs.get(
|
|
262
|
-
sufix=kwargs.get(
|
|
263
|
-
rotateatutc=kwargs.get(
|
|
264
|
-
daystokeep=kwargs.get(
|
|
251
|
+
level=kwargs.get("level", config.level),
|
|
252
|
+
name=kwargs.get("name", config.name),
|
|
253
|
+
directory=kwargs.get("directory", config.directory),
|
|
254
|
+
filenames=kwargs.get("filenames", config.filenames),
|
|
255
|
+
encoding=kwargs.get("encoding", config.encoding),
|
|
256
|
+
datefmt=kwargs.get("datefmt", config.datefmt),
|
|
257
|
+
timezone=kwargs.get("timezone", config.timezone),
|
|
258
|
+
streamhandler=kwargs.get("streamhandler", config.streamhandler),
|
|
259
|
+
showlocation=kwargs.get("showlocation", config.showlocation),
|
|
260
|
+
maxmbytes=kwargs.get("maxmbytes", config.maxmbytes),
|
|
261
|
+
when=kwargs.get("when", config.when),
|
|
262
|
+
sufix=kwargs.get("sufix", config.sufix),
|
|
263
|
+
rotateatutc=kwargs.get("rotateatutc", config.rotateatutc),
|
|
264
|
+
daystokeep=kwargs.get("daystokeep", config.daystokeep),
|
|
265
265
|
)
|
|
266
266
|
|
|
267
267
|
# Convert enum values to strings for logger classes
|
|
@@ -314,12 +314,12 @@ class LoggerFactory:
|
|
|
314
314
|
|
|
315
315
|
@staticmethod
|
|
316
316
|
def create_basic_logger(
|
|
317
|
-
level:
|
|
318
|
-
name:
|
|
319
|
-
encoding:
|
|
320
|
-
datefmt:
|
|
321
|
-
timezone:
|
|
322
|
-
showlocation:
|
|
317
|
+
level: LogLevel | str | None = None,
|
|
318
|
+
name: str | None = None,
|
|
319
|
+
encoding: str | None = None,
|
|
320
|
+
datefmt: str | None = None,
|
|
321
|
+
timezone: str | None = None,
|
|
322
|
+
showlocation: bool | None = None,
|
|
323
323
|
) -> logging.Logger:
|
|
324
324
|
"""Convenience method for creating a basic logger"""
|
|
325
325
|
return LoggerFactory.create_logger(
|
|
@@ -334,17 +334,17 @@ class LoggerFactory:
|
|
|
334
334
|
|
|
335
335
|
@staticmethod
|
|
336
336
|
def create_size_rotating_logger(
|
|
337
|
-
level:
|
|
338
|
-
name:
|
|
339
|
-
directory:
|
|
340
|
-
filenames:
|
|
341
|
-
maxmbytes:
|
|
342
|
-
daystokeep:
|
|
343
|
-
encoding:
|
|
344
|
-
datefmt:
|
|
345
|
-
timezone:
|
|
346
|
-
streamhandler:
|
|
347
|
-
showlocation:
|
|
337
|
+
level: LogLevel | str | None = None,
|
|
338
|
+
name: str | None = None,
|
|
339
|
+
directory: str | None = None,
|
|
340
|
+
filenames: list | tuple | None = None,
|
|
341
|
+
maxmbytes: int | None = None,
|
|
342
|
+
daystokeep: int | None = None,
|
|
343
|
+
encoding: str | None = None,
|
|
344
|
+
datefmt: str | None = None,
|
|
345
|
+
timezone: str | None = None,
|
|
346
|
+
streamhandler: bool | None = None,
|
|
347
|
+
showlocation: bool | None = None,
|
|
348
348
|
) -> logging.Logger:
|
|
349
349
|
"""Convenience method for creating a size rotating logger"""
|
|
350
350
|
return LoggerFactory.create_logger(
|
|
@@ -364,19 +364,19 @@ class LoggerFactory:
|
|
|
364
364
|
|
|
365
365
|
@staticmethod
|
|
366
366
|
def create_timed_rotating_logger(
|
|
367
|
-
level:
|
|
368
|
-
name:
|
|
369
|
-
directory:
|
|
370
|
-
filenames:
|
|
371
|
-
when:
|
|
372
|
-
sufix:
|
|
373
|
-
daystokeep:
|
|
374
|
-
encoding:
|
|
375
|
-
datefmt:
|
|
376
|
-
timezone:
|
|
377
|
-
streamhandler:
|
|
378
|
-
showlocation:
|
|
379
|
-
rotateatutc:
|
|
367
|
+
level: LogLevel | str | None = None,
|
|
368
|
+
name: str | None = None,
|
|
369
|
+
directory: str | None = None,
|
|
370
|
+
filenames: list | tuple | None = None,
|
|
371
|
+
when: RotateWhen | str | None = None,
|
|
372
|
+
sufix: str | None = None,
|
|
373
|
+
daystokeep: int | None = None,
|
|
374
|
+
encoding: str | None = None,
|
|
375
|
+
datefmt: str | None = None,
|
|
376
|
+
timezone: str | None = None,
|
|
377
|
+
streamhandler: bool | None = None,
|
|
378
|
+
showlocation: bool | None = None,
|
|
379
|
+
rotateatutc: bool | None = None,
|
|
380
380
|
) -> logging.Logger:
|
|
381
381
|
"""Convenience method for creating a timed rotating logger"""
|
|
382
382
|
return LoggerFactory.create_logger(
|
|
@@ -432,12 +432,12 @@ class BasicLog(_LoggerMixin):
|
|
|
432
432
|
|
|
433
433
|
def __init__(
|
|
434
434
|
self,
|
|
435
|
-
level:
|
|
436
|
-
name:
|
|
437
|
-
encoding:
|
|
438
|
-
datefmt:
|
|
439
|
-
timezone:
|
|
440
|
-
showlocation:
|
|
435
|
+
level: LogLevel | str | None = None,
|
|
436
|
+
name: str | None = None,
|
|
437
|
+
encoding: str | None = None,
|
|
438
|
+
datefmt: str | None = None,
|
|
439
|
+
timezone: str | None = None,
|
|
440
|
+
showlocation: bool | None = None,
|
|
441
441
|
):
|
|
442
442
|
self._logger = LoggerFactory.create_basic_logger(
|
|
443
443
|
level=level,
|
|
@@ -465,17 +465,17 @@ class SizeRotatingLog(_LoggerMixin):
|
|
|
465
465
|
|
|
466
466
|
def __init__(
|
|
467
467
|
self,
|
|
468
|
-
level:
|
|
469
|
-
name:
|
|
470
|
-
directory:
|
|
471
|
-
filenames:
|
|
472
|
-
maxmbytes:
|
|
473
|
-
daystokeep:
|
|
474
|
-
encoding:
|
|
475
|
-
datefmt:
|
|
476
|
-
timezone:
|
|
477
|
-
streamhandler:
|
|
478
|
-
showlocation:
|
|
468
|
+
level: LogLevel | str | None = None,
|
|
469
|
+
name: str | None = None,
|
|
470
|
+
directory: str | None = None,
|
|
471
|
+
filenames: list | tuple | None = None,
|
|
472
|
+
maxmbytes: int | None = None,
|
|
473
|
+
daystokeep: int | None = None,
|
|
474
|
+
encoding: str | None = None,
|
|
475
|
+
datefmt: str | None = None,
|
|
476
|
+
timezone: str | None = None,
|
|
477
|
+
streamhandler: bool | None = None,
|
|
478
|
+
showlocation: bool | None = None,
|
|
479
479
|
):
|
|
480
480
|
self._logger = LoggerFactory.create_size_rotating_logger(
|
|
481
481
|
level=level,
|
|
@@ -508,19 +508,19 @@ class TimedRotatingLog(_LoggerMixin):
|
|
|
508
508
|
|
|
509
509
|
def __init__(
|
|
510
510
|
self,
|
|
511
|
-
level:
|
|
512
|
-
name:
|
|
513
|
-
directory:
|
|
514
|
-
filenames:
|
|
515
|
-
when:
|
|
516
|
-
sufix:
|
|
517
|
-
daystokeep:
|
|
518
|
-
encoding:
|
|
519
|
-
datefmt:
|
|
520
|
-
timezone:
|
|
521
|
-
streamhandler:
|
|
522
|
-
showlocation:
|
|
523
|
-
rotateatutc:
|
|
511
|
+
level: LogLevel | str | None = None,
|
|
512
|
+
name: str | None = None,
|
|
513
|
+
directory: str | None = None,
|
|
514
|
+
filenames: list | tuple | None = None,
|
|
515
|
+
when: RotateWhen | str | None = None,
|
|
516
|
+
sufix: str | None = None,
|
|
517
|
+
daystokeep: int | None = None,
|
|
518
|
+
encoding: str | None = None,
|
|
519
|
+
datefmt: str | None = None,
|
|
520
|
+
timezone: str | None = None,
|
|
521
|
+
streamhandler: bool | None = None,
|
|
522
|
+
showlocation: bool | None = None,
|
|
523
|
+
rotateatutc: bool | None = None,
|
|
524
524
|
):
|
|
525
525
|
self._logger = LoggerFactory.create_timed_rotating_logger(
|
|
526
526
|
level=level,
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
from datetime import datetime, timedelta
|
|
2
|
-
from datetime import timezone as dttz
|
|
3
1
|
import errno
|
|
4
|
-
from functools import lru_cache
|
|
5
2
|
import gzip
|
|
6
3
|
import logging
|
|
7
4
|
import logging.handlers
|
|
8
5
|
import os
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
|
|
11
6
|
import shutil
|
|
12
7
|
import sys
|
|
13
8
|
import threading
|
|
14
9
|
import time
|
|
15
|
-
from
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from datetime import UTC, datetime, timedelta
|
|
12
|
+
from functools import lru_cache
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from pythonLogs.core.constants import DEFAULT_FILE_MODE, LEVEL_MAP
|
|
16
15
|
from zoneinfo import ZoneInfo
|
|
17
16
|
|
|
18
17
|
|
|
@@ -21,15 +20,17 @@ class RotatingLogMixin:
|
|
|
21
20
|
|
|
22
21
|
logger: logging.Logger | None
|
|
23
22
|
|
|
23
|
+
def init(self) -> None: ...
|
|
24
|
+
|
|
24
25
|
def __enter__(self):
|
|
25
26
|
"""Context manager entry."""
|
|
26
|
-
if not hasattr(self,
|
|
27
|
+
if not hasattr(self, "logger") or self.logger is None:
|
|
27
28
|
self.init()
|
|
28
29
|
return self.logger
|
|
29
30
|
|
|
30
31
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
31
32
|
"""Context manager exit with automatic cleanup."""
|
|
32
|
-
if hasattr(self,
|
|
33
|
+
if hasattr(self, "logger"):
|
|
33
34
|
cleanup_logger_handlers(self.logger)
|
|
34
35
|
|
|
35
36
|
@staticmethod
|
|
@@ -39,7 +40,7 @@ class RotatingLogMixin:
|
|
|
39
40
|
|
|
40
41
|
|
|
41
42
|
# Global cache for checked directories with thread safety and size limits
|
|
42
|
-
_checked_directories:
|
|
43
|
+
_checked_directories: set[str] = set()
|
|
43
44
|
_directory_lock = threading.Lock()
|
|
44
45
|
_max_cached_directories = 500 # Limit cache size to prevent unbounded growth
|
|
45
46
|
|
|
@@ -109,7 +110,7 @@ def check_directory_permissions(directory_path: str) -> None:
|
|
|
109
110
|
except PermissionError as e:
|
|
110
111
|
err_msg = f"Unable to create directory | {directory_path}"
|
|
111
112
|
write_stderr(f"{err_msg} | {type(e).__name__}: {e}")
|
|
112
|
-
raise PermissionError(err_msg)
|
|
113
|
+
raise PermissionError(err_msg) from e
|
|
113
114
|
|
|
114
115
|
# Add to cache with size limit enforcement
|
|
115
116
|
if len(_checked_directories) >= _max_cached_directories:
|
|
@@ -129,7 +130,7 @@ def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
|
|
|
129
130
|
try:
|
|
130
131
|
if file_path.stat().st_mtime < cutoff_time.timestamp():
|
|
131
132
|
file_path.unlink()
|
|
132
|
-
except
|
|
133
|
+
except OSError as e:
|
|
133
134
|
write_stderr(f"Unable to delete old log | {file_path} | {type(e).__name__}: {e}")
|
|
134
135
|
except OSError as e:
|
|
135
136
|
write_stderr(f"Unable to scan directory for old logs | {logs_dir} | {type(e).__name__}: {e}")
|
|
@@ -196,7 +197,7 @@ def write_stderr(msg: str) -> None:
|
|
|
196
197
|
# Use local timezone
|
|
197
198
|
dt = datetime.now()
|
|
198
199
|
else:
|
|
199
|
-
dt = datetime.now(
|
|
200
|
+
dt = datetime.now(UTC).astimezone(tz)
|
|
200
201
|
dt_timezone = dt.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
|
|
201
202
|
sys.stderr.write(f"[{dt_timezone}]:[ERROR]:{msg}\n")
|
|
202
203
|
except (OSError, ValueError, KeyError):
|
|
@@ -286,7 +287,7 @@ def gzip_file_with_sufix(file_path: str, sufix: str) -> str | None:
|
|
|
286
287
|
# Final attempt failed or not Windows - treat as regular error
|
|
287
288
|
write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
|
|
288
289
|
raise e
|
|
289
|
-
except
|
|
290
|
+
except OSError as e:
|
|
290
291
|
write_stderr(f"Unable to gzip log file | {file_path} | {type(e).__name__}: {e}")
|
|
291
292
|
raise e
|
|
292
293
|
|
|
@@ -324,7 +325,7 @@ def get_timezone_function(time_zone: str) -> Callable:
|
|
|
324
325
|
|
|
325
326
|
|
|
326
327
|
# Shared handler cleanup utility
|
|
327
|
-
def cleanup_logger_handlers(logger:
|
|
328
|
+
def cleanup_logger_handlers(logger: logging.Logger | None) -> None:
|
|
328
329
|
"""Clean up logger resources by closing all handlers.
|
|
329
330
|
|
|
330
331
|
This is a centralized utility to ensure consistent cleanup behavior
|
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
from . import log_utils
|
|
2
|
-
from functools import lru_cache
|
|
3
1
|
import logging
|
|
4
2
|
import threading
|
|
5
|
-
from typing import Any, Dict, Optional, Set
|
|
6
3
|
import weakref
|
|
4
|
+
from . import log_utils
|
|
5
|
+
from .settings import get_log_settings
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from typing import Any
|
|
7
8
|
|
|
8
9
|
# Formatter cache to reduce memory usage for identical formatters
|
|
9
|
-
_formatter_cache:
|
|
10
|
+
_formatter_cache: dict[str, logging.Formatter] = {}
|
|
10
11
|
_formatter_cache_lock = threading.Lock()
|
|
11
|
-
_max_formatters =
|
|
12
|
+
_max_formatters = get_log_settings().max_formatters
|
|
12
13
|
|
|
13
14
|
|
|
14
|
-
def get_cached_formatter(format_string: str, datefmt:
|
|
15
|
+
def get_cached_formatter(format_string: str, datefmt: str | None = None) -> logging.Formatter:
|
|
15
16
|
"""Get a cached formatter or create and cache a new one.
|
|
16
17
|
|
|
17
18
|
This reduces memory usage by reusing formatter instances with
|
|
@@ -66,7 +67,7 @@ def clear_directory_cache() -> None:
|
|
|
66
67
|
|
|
67
68
|
|
|
68
69
|
# Weak reference registry for tracking active loggers without preventing GC
|
|
69
|
-
_active_loggers:
|
|
70
|
+
_active_loggers: set[weakref.ReferenceType] = set()
|
|
70
71
|
_weak_ref_lock = threading.Lock()
|
|
71
72
|
|
|
72
73
|
|
|
@@ -103,7 +104,7 @@ def get_active_logger_count() -> int:
|
|
|
103
104
|
return len(_active_loggers)
|
|
104
105
|
|
|
105
106
|
|
|
106
|
-
def get_memory_stats() ->
|
|
107
|
+
def get_memory_stats() -> dict[str, Any]:
|
|
107
108
|
"""Get memory usage statistics for the logging system.
|
|
108
109
|
|
|
109
110
|
Returns:
|
|
@@ -125,13 +126,13 @@ def get_memory_stats() -> Dict[str, Any]:
|
|
|
125
126
|
directory_stats = log_utils.get_directory_cache_stats()
|
|
126
127
|
|
|
127
128
|
return {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
129
|
+
"registry_size": registry_size,
|
|
130
|
+
"formatter_cache_size": formatter_cache_size,
|
|
131
|
+
"directory_cache_size": directory_stats["cached_directories"],
|
|
132
|
+
"active_logger_count": get_active_logger_count(),
|
|
133
|
+
"max_registry_size": factory_limits["max_loggers"],
|
|
134
|
+
"max_formatter_cache": _max_formatters,
|
|
135
|
+
"max_directory_cache": directory_stats["max_directories"],
|
|
135
136
|
}
|
|
136
137
|
|
|
137
138
|
|
|
@@ -153,7 +154,7 @@ def optimize_lru_cache_sizes() -> None:
|
|
|
153
154
|
log_utils.get_stderr_timezone = lru_cache(maxsize=4)(log_utils.get_stderr_timezone.__wrapped__)
|
|
154
155
|
|
|
155
156
|
|
|
156
|
-
def force_garbage_collection() ->
|
|
157
|
+
def force_garbage_collection() -> dict[str, int]:
|
|
157
158
|
"""Force garbage collection and return collection statistics.
|
|
158
159
|
|
|
159
160
|
This can be useful for testing memory leaks or forcing cleanup
|
|
@@ -172,7 +173,7 @@ def force_garbage_collection() -> Dict[str, int]:
|
|
|
172
173
|
collected = gc.collect()
|
|
173
174
|
|
|
174
175
|
return {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
"objects_collected": collected,
|
|
177
|
+
"garbage_count": len(gc.garbage),
|
|
178
|
+
"reference_cycles": gc.get_count(),
|
|
178
179
|
}
|
|
@@ -16,6 +16,14 @@ from pythonLogs.core.constants import (
|
|
|
16
16
|
_dotenv_loaded = False
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
def _ensure_dotenv_loaded() -> None:
|
|
20
|
+
"""Ensure dotenv is loaded only once."""
|
|
21
|
+
global _dotenv_loaded
|
|
22
|
+
if not _dotenv_loaded:
|
|
23
|
+
load_dotenv()
|
|
24
|
+
_dotenv_loaded = True
|
|
25
|
+
|
|
26
|
+
|
|
19
27
|
class LogSettings(BaseSettings):
|
|
20
28
|
"""If any ENV variable is omitted, it falls back to default values here"""
|
|
21
29
|
|
|
@@ -67,6 +75,10 @@ class LogSettings(BaseSettings):
|
|
|
67
75
|
default=100,
|
|
68
76
|
description="Maximum number of loggers to track in memory",
|
|
69
77
|
)
|
|
78
|
+
max_formatters: int = Field(
|
|
79
|
+
default=50,
|
|
80
|
+
description="Maximum number of formatters to cache in memory",
|
|
81
|
+
)
|
|
70
82
|
logger_ttl_seconds: int = Field(
|
|
71
83
|
default=3600,
|
|
72
84
|
description="Time-to-live in seconds for logger references",
|
|
@@ -95,9 +107,18 @@ class LogSettings(BaseSettings):
|
|
|
95
107
|
|
|
96
108
|
@lru_cache(maxsize=1)
|
|
97
109
|
def get_log_settings() -> LogSettings:
|
|
98
|
-
"""Get cached log settings instance to avoid repeated instantiation"""
|
|
99
|
-
|
|
100
|
-
if not _dotenv_loaded:
|
|
101
|
-
load_dotenv()
|
|
102
|
-
_dotenv_loaded = True
|
|
110
|
+
"""Get cached log settings instance to avoid repeated instantiation."""
|
|
111
|
+
_ensure_dotenv_loaded()
|
|
103
112
|
return LogSettings()
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def clear_settings_cache(reload_env: bool = True) -> None:
|
|
116
|
+
"""Clear log settings cache. Next call to get_log_settings() will create fresh instance.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
reload_env: If True, also reset dotenv loaded flag to reload .env on next access
|
|
120
|
+
"""
|
|
121
|
+
global _dotenv_loaded
|
|
122
|
+
get_log_settings.cache_clear()
|
|
123
|
+
if reload_env:
|
|
124
|
+
_dotenv_loaded = False
|
|
@@ -1,23 +1,24 @@
|
|
|
1
1
|
import functools
|
|
2
2
|
import threading
|
|
3
|
-
from
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from typing import Any, TypeVar
|
|
4
5
|
|
|
5
|
-
F = TypeVar(
|
|
6
|
+
F = TypeVar("F", bound=Callable[..., Any])
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ThreadSafeMeta(type):
|
|
9
10
|
"""Metaclass that automatically adds thread safety to class methods."""
|
|
10
11
|
|
|
11
|
-
def __new__(mcs, name: str, bases: tuple, namespace:
|
|
12
|
+
def __new__(mcs, name: str, bases: tuple, namespace: dict[str, Any], **kwargs):
|
|
12
13
|
# Create the class first
|
|
13
14
|
cls = super().__new__(mcs, name, bases, namespace)
|
|
14
15
|
|
|
15
16
|
# Add a class-level lock if not already present
|
|
16
|
-
if not hasattr(cls,
|
|
17
|
+
if not hasattr(cls, "_lock"):
|
|
17
18
|
cls._lock = threading.RLock()
|
|
18
19
|
|
|
19
20
|
# Get methods that should be thread-safe (exclude private/dunder methods)
|
|
20
|
-
thread_safe_methods = getattr(cls,
|
|
21
|
+
thread_safe_methods = getattr(cls, "_thread_safe_methods", None)
|
|
21
22
|
if thread_safe_methods is None:
|
|
22
23
|
# Auto-detect public methods that modify state
|
|
23
24
|
thread_safe_methods = [
|
|
@@ -25,8 +26,8 @@ class ThreadSafeMeta(type):
|
|
|
25
26
|
for method_name in namespace
|
|
26
27
|
if (
|
|
27
28
|
callable(getattr(cls, method_name, None))
|
|
28
|
-
and not method_name.startswith(
|
|
29
|
-
and method_name not in [
|
|
29
|
+
and not method_name.startswith("_")
|
|
30
|
+
and method_name not in ["__enter__", "__exit__", "__init__"]
|
|
30
31
|
)
|
|
31
32
|
]
|
|
32
33
|
|
|
@@ -47,12 +48,12 @@ def thread_safe(func: F) -> F:
|
|
|
47
48
|
@functools.wraps(func)
|
|
48
49
|
def wrapper(self, *args, **kwargs):
|
|
49
50
|
# Use instance lock if available, otherwise class lock
|
|
50
|
-
lock = getattr(self,
|
|
51
|
+
lock = getattr(self, "_lock", None)
|
|
51
52
|
if lock is None:
|
|
52
53
|
# Check if class has lock, if not create one
|
|
53
|
-
if not hasattr(self.__class__,
|
|
54
|
-
|
|
55
|
-
lock =
|
|
54
|
+
if not hasattr(self.__class__, "_lock"):
|
|
55
|
+
self.__class__._lock = threading.RLock()
|
|
56
|
+
lock = self.__class__._lock
|
|
56
57
|
|
|
57
58
|
with lock:
|
|
58
59
|
return func(self, *args, **kwargs)
|
|
@@ -60,36 +61,36 @@ def thread_safe(func: F) -> F:
|
|
|
60
61
|
return wrapper
|
|
61
62
|
|
|
62
63
|
|
|
63
|
-
def _get_wrappable_methods(cls:
|
|
64
|
+
def _get_wrappable_methods(cls: type) -> list:
|
|
64
65
|
"""Helper function to get methods that should be made thread-safe."""
|
|
65
66
|
return [
|
|
66
67
|
method_name
|
|
67
68
|
for method_name in dir(cls)
|
|
68
69
|
if (
|
|
69
70
|
callable(getattr(cls, method_name, None))
|
|
70
|
-
and not method_name.startswith(
|
|
71
|
-
and method_name not in [
|
|
71
|
+
and not method_name.startswith("_")
|
|
72
|
+
and method_name not in ["__enter__", "__exit__", "__init__"]
|
|
72
73
|
)
|
|
73
74
|
]
|
|
74
75
|
|
|
75
76
|
|
|
76
|
-
def _ensure_class_has_lock(cls:
|
|
77
|
+
def _ensure_class_has_lock(cls: type) -> None:
|
|
77
78
|
"""Ensure the class has a lock attribute."""
|
|
78
|
-
if not hasattr(cls,
|
|
79
|
+
if not hasattr(cls, "_lock"):
|
|
79
80
|
cls._lock = threading.RLock()
|
|
80
81
|
|
|
81
82
|
|
|
82
|
-
def _should_wrap_method(cls:
|
|
83
|
+
def _should_wrap_method(cls: type, method_name: str, original_method: Any) -> bool:
|
|
83
84
|
"""Check if a method should be wrapped with thread safety."""
|
|
84
85
|
return (
|
|
85
|
-
hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method,
|
|
86
|
+
hasattr(cls, method_name) and callable(original_method) and not hasattr(original_method, "_thread_safe_wrapped")
|
|
86
87
|
)
|
|
87
88
|
|
|
88
89
|
|
|
89
90
|
def auto_thread_safe(thread_safe_methods: list = None):
|
|
90
91
|
"""Class decorator that adds automatic thread safety to specified methods."""
|
|
91
92
|
|
|
92
|
-
def decorator(cls:
|
|
93
|
+
def decorator(cls: type) -> type:
|
|
93
94
|
_ensure_class_has_lock(cls)
|
|
94
95
|
|
|
95
96
|
# Store thread-safe methods list
|
|
@@ -116,21 +117,21 @@ class AutoThreadSafe:
|
|
|
116
117
|
"""Base class that provides automatic thread safety for all public methods."""
|
|
117
118
|
|
|
118
119
|
def __init__(self):
|
|
119
|
-
if not hasattr(self,
|
|
120
|
+
if not hasattr(self, "_lock"):
|
|
120
121
|
self._lock = threading.RLock()
|
|
121
122
|
|
|
122
123
|
def __init_subclass__(cls, **kwargs):
|
|
123
124
|
super().__init_subclass__(**kwargs)
|
|
124
125
|
|
|
125
126
|
# Add class-level lock
|
|
126
|
-
if not hasattr(cls,
|
|
127
|
+
if not hasattr(cls, "_lock"):
|
|
127
128
|
cls._lock = threading.RLock()
|
|
128
129
|
|
|
129
130
|
# Auto-wrap public methods
|
|
130
131
|
for attr_name in dir(cls):
|
|
131
|
-
if not attr_name.startswith(
|
|
132
|
+
if not attr_name.startswith("_"):
|
|
132
133
|
attr = getattr(cls, attr_name)
|
|
133
|
-
if callable(attr) and not hasattr(attr,
|
|
134
|
+
if callable(attr) and not hasattr(attr, "_thread_safe_wrapped"):
|
|
134
135
|
wrapped_attr = thread_safe(attr)
|
|
135
136
|
wrapped_attr._thread_safe_wrapped = True
|
|
136
137
|
setattr(cls, attr_name, wrapped_attr)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import logging.handlers
|
|
2
2
|
import os
|
|
3
|
+
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from pythonLogs.core.constants import MB_TO_BYTES
|
|
5
6
|
from pythonLogs.core.log_utils import (
|
|
@@ -17,10 +18,9 @@ from pythonLogs.core.log_utils import (
|
|
|
17
18
|
from pythonLogs.core.memory_utils import register_logger_weakref
|
|
18
19
|
from pythonLogs.core.settings import get_log_settings
|
|
19
20
|
from pythonLogs.core.thread_safety import auto_thread_safe
|
|
20
|
-
import re
|
|
21
21
|
|
|
22
22
|
|
|
23
|
-
@auto_thread_safe([
|
|
23
|
+
@auto_thread_safe(["init"])
|
|
24
24
|
class SizeRotatingLog(RotatingLogMixin):
|
|
25
25
|
"""Size-based rotating logger with context manager support for automatic resource cleanup."""
|
|
26
26
|
|
|
@@ -16,7 +16,7 @@ from pythonLogs.core.settings import get_log_settings
|
|
|
16
16
|
from pythonLogs.core.thread_safety import auto_thread_safe
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
@auto_thread_safe([
|
|
19
|
+
@auto_thread_safe(["init"])
|
|
20
20
|
class TimedRotatingLog(RotatingLogMixin):
|
|
21
21
|
"""
|
|
22
22
|
Time-based rotating logger with context manager support for automatic resource cleanup.
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
from importlib.metadata import version
|
|
2
|
-
import logging
|
|
3
|
-
from pythonLogs.core.constants import LogLevel, RotateWhen
|
|
4
|
-
from pythonLogs.core.factory import BasicLog, SizeRotatingLog, TimedRotatingLog
|
|
5
|
-
from typing import Literal, NamedTuple
|
|
6
|
-
|
|
7
|
-
__all__ = (
|
|
8
|
-
"BasicLog",
|
|
9
|
-
"SizeRotatingLog",
|
|
10
|
-
"TimedRotatingLog",
|
|
11
|
-
"LogLevel",
|
|
12
|
-
"RotateWhen",
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
__title__ = "pythonLogs"
|
|
16
|
-
__author__ = "Daniel Costa"
|
|
17
|
-
__email__ = "danieldcsta@gmail.com>"
|
|
18
|
-
__license__ = "MIT"
|
|
19
|
-
__copyright__ = "Copyright 2024-present DDC Softwares"
|
|
20
|
-
_req_python_version = (3, 12, 0)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
try:
|
|
24
|
-
_version = tuple(int(x) for x in version(__title__).split("."))
|
|
25
|
-
except ModuleNotFoundError:
|
|
26
|
-
_version = (0, 0, 0)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
class VersionInfo(NamedTuple):
|
|
30
|
-
major: int
|
|
31
|
-
minor: int
|
|
32
|
-
micro: int
|
|
33
|
-
releaselevel: Literal["alpha", "beta", "candidate", "final"]
|
|
34
|
-
serial: int
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
__version__ = _version
|
|
38
|
-
__version_info__: VersionInfo = VersionInfo(
|
|
39
|
-
major=__version__[0],
|
|
40
|
-
minor=__version__[1],
|
|
41
|
-
micro=__version__[2],
|
|
42
|
-
releaselevel="final",
|
|
43
|
-
serial=0,
|
|
44
|
-
)
|
|
45
|
-
__req_python_version__: VersionInfo = VersionInfo(
|
|
46
|
-
major=_req_python_version[0],
|
|
47
|
-
minor=_req_python_version[1],
|
|
48
|
-
micro=_req_python_version[2],
|
|
49
|
-
releaselevel="final",
|
|
50
|
-
serial=0,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
logging.getLogger(__name__).addHandler(logging.NullHandler())
|
|
54
|
-
|
|
55
|
-
del (
|
|
56
|
-
logging,
|
|
57
|
-
NamedTuple,
|
|
58
|
-
Literal,
|
|
59
|
-
VersionInfo,
|
|
60
|
-
version,
|
|
61
|
-
_version,
|
|
62
|
-
_req_python_version,
|
|
63
|
-
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|