pythonLogs 3.0.12__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-3.0.12/LICENSE +21 -0
- pythonlogs-3.0.12/PKG-INFO +192 -0
- pythonlogs-3.0.12/README.md +161 -0
- pythonlogs-3.0.12/pyproject.toml +62 -0
- pythonlogs-3.0.12/pythonLogs/.env.example +17 -0
- pythonlogs-3.0.12/pythonLogs/__init__.py +55 -0
- pythonlogs-3.0.12/pythonLogs/basic_log.py +32 -0
- pythonlogs-3.0.12/pythonLogs/log_utils.py +264 -0
- pythonlogs-3.0.12/pythonLogs/settings.py +45 -0
- pythonlogs-3.0.12/pythonLogs/size_rotating.py +105 -0
- pythonlogs-3.0.12/pythonLogs/timed_rotating.py +94 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present ddc
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: pythonLogs
|
|
3
|
+
Version: 3.0.12
|
|
4
|
+
Summary: Easy logs with rotations
|
|
5
|
+
Home-page: https://pypi.org/project/pythonLogs
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: python3,python-3,python,log,logging,logger,logutils,log-utils,pythonLogs
|
|
8
|
+
Author: Daniel Costa
|
|
9
|
+
Author-email: danieldcsta@gmail.com
|
|
10
|
+
Maintainer: Daniel Costa
|
|
11
|
+
Requires-Python: >=3.10,<4.0
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: Environment :: Other Environment
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Natural Language :: English
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Requires-Dist: pydantic-settings (>=2.7.1,<3.0.0)
|
|
26
|
+
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
|
27
|
+
Requires-Dist: pytz (>=2024.2,<2025.0)
|
|
28
|
+
Project-URL: Repository, https://github.com/ddc/pythonLogs
|
|
29
|
+
Description-Content-Type: text/markdown
|
|
30
|
+
|
|
31
|
+
# Easy logs with rotations
|
|
32
|
+
|
|
33
|
+
[](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
|
|
34
|
+
[](https://github.com/ddc/pythonLogs/blob/main/LICENSE)
|
|
35
|
+
[](https://pypi.python.org/pypi/pythonLogs)
|
|
36
|
+
[](https://pepy.tech/projects/pythonLogs)
|
|
37
|
+
[](https://codecov.io/gh/ddc/pythonLogs)
|
|
38
|
+
[](https://github.com/psf/black)
|
|
39
|
+
[](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
|
|
40
|
+
[](https://www.python.org)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Logs
|
|
45
|
+
+ Parameters for all classes are declared as OPTIONAL
|
|
46
|
+
+ If any [.env](./pythonLogs/.env.example) variable is omitted, it falls back to default values here: [settings.py](pythonLogs/settings.py)
|
|
47
|
+
+ Function arguments will overwrite any env variable
|
|
48
|
+
+ Timezone parameter can also accept `localtime`, default to `UTC`
|
|
49
|
+
+ This parameter is only to display the timezone datetime inside the log file
|
|
50
|
+
+ For timed rotation, only UTC and localtime are supported, meaning it will rotate at UTC or localtime
|
|
51
|
+
+ env variable to change between UTC and localtime is `LOG_ROTATE_AT_UTC` and default to True
|
|
52
|
+
+ Streamhandler parameter will add stream handler along with file handler
|
|
53
|
+
+ Showlocation parameter will show the filename and the line number where the message originated
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# Install
|
|
59
|
+
```shell
|
|
60
|
+
pip install pythonLogs
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# BasicLog
|
|
66
|
+
+ Setup Logging
|
|
67
|
+
+ This is just a basic log, it does not use any file
|
|
68
|
+
```python
|
|
69
|
+
from pythonLogs import BasicLog
|
|
70
|
+
logger = BasicLog(
|
|
71
|
+
level="debug",
|
|
72
|
+
name="app",
|
|
73
|
+
timezone="America/Sao_Paulo",
|
|
74
|
+
showlocation=False,
|
|
75
|
+
).init()
|
|
76
|
+
logger.warning("This is a warning example")
|
|
77
|
+
```
|
|
78
|
+
#### Example of output
|
|
79
|
+
`[2024-10-08T19:08:56.918-0300]:[WARNING]:[app]:This is a warning example`
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# SizeRotatingLog
|
|
86
|
+
+ Setup Logging
|
|
87
|
+
+ Logs will rotate based on the file size using the `maxmbytes` variable
|
|
88
|
+
+ Rotated logs will have a sequence number starting from 1: `app.log_1.gz, app.log_2.gz`
|
|
89
|
+
+ Logs will be deleted based on the `daystokeep` variable, defaults to 30
|
|
90
|
+
```python
|
|
91
|
+
from pythonLogs import SizeRotatingLog
|
|
92
|
+
logger = SizeRotatingLog(
|
|
93
|
+
level="debug",
|
|
94
|
+
name="app",
|
|
95
|
+
directory="/app/logs",
|
|
96
|
+
filenames=["main.log", "app1.log"],
|
|
97
|
+
maxmbytes=5,
|
|
98
|
+
daystokeep=7,
|
|
99
|
+
timezone="America/Chicago",
|
|
100
|
+
streamhandler=True,
|
|
101
|
+
showlocation=False
|
|
102
|
+
).init()
|
|
103
|
+
logger.warning("This is a warning example")
|
|
104
|
+
```
|
|
105
|
+
#### Example of output
|
|
106
|
+
`[2024-10-08T19:08:56.918-0500]:[WARNING]:[app]:This is a warning example`
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# TimedRotatingLog
|
|
113
|
+
+ Setup Logging
|
|
114
|
+
+ Logs will rotate based on `when` variable to a `.gz` file, defaults to `midnight`
|
|
115
|
+
+ Rotated log will have the sufix variable on its name: `app_20240816.log.gz`
|
|
116
|
+
+ Logs will be deleted based on the `daystokeep` variable, defaults to 30
|
|
117
|
+
+ Current 'when' events supported:
|
|
118
|
+
+ midnight — roll over at midnight
|
|
119
|
+
+ W{0-6} - roll over on a certain day; 0 - Monday
|
|
120
|
+
```python
|
|
121
|
+
from pythonLogs import TimedRotatingLog
|
|
122
|
+
logger = TimedRotatingLog(
|
|
123
|
+
level="debug",
|
|
124
|
+
name="app",
|
|
125
|
+
directory="/app/logs",
|
|
126
|
+
filenames=["main.log", "app2.log"],
|
|
127
|
+
when="midnight",
|
|
128
|
+
daystokeep=7,
|
|
129
|
+
timezone="UTC",
|
|
130
|
+
streamhandler=True,
|
|
131
|
+
showlocation=False
|
|
132
|
+
).init()
|
|
133
|
+
logger.warning("This is a warning example")
|
|
134
|
+
```
|
|
135
|
+
#### Example of output
|
|
136
|
+
`[2024-10-08T19:08:56.918-0000]:[WARNING]:[app]:This is a warning example`
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
## Env Variables (Optional)
|
|
143
|
+
```
|
|
144
|
+
LOG_LEVEL=DEBUG
|
|
145
|
+
LOG_TIMEZONE=America/Chicago
|
|
146
|
+
LOG_ENCODING=UTF-8
|
|
147
|
+
LOG_APPNAME=app
|
|
148
|
+
LOG_FILENAME=app.log
|
|
149
|
+
LOG_DIRECTORY=/app/logs
|
|
150
|
+
LOG_DAYS_TO_KEEP=30
|
|
151
|
+
LOG_STREAM_HANDLER=True
|
|
152
|
+
LOG_SHOW_LOCATION=False
|
|
153
|
+
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
|
|
154
|
+
|
|
155
|
+
# SizeRotatingLog
|
|
156
|
+
LOG_MAX_FILE_SIZE_MB=10
|
|
157
|
+
|
|
158
|
+
# TimedRotatingLog
|
|
159
|
+
LOG_ROTATE_WHEN=midnight
|
|
160
|
+
LOG_ROTATE_AT_UTC=True
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
# Source Code
|
|
167
|
+
### Build
|
|
168
|
+
```shell
|
|
169
|
+
poetry build -f wheel
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# Run Tests and Get Coverage Report using Poe
|
|
175
|
+
```shell
|
|
176
|
+
poetry update --with test
|
|
177
|
+
poe tests
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# License
|
|
183
|
+
Released under the [MIT License](LICENSE)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# Buy me a cup of coffee
|
|
189
|
+
+ [GitHub Sponsor](https://github.com/sponsors/ddc)
|
|
190
|
+
+ [ko-fi](https://ko-fi.com/ddcsta)
|
|
191
|
+
+ [Paypal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
|
|
192
|
+
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Easy logs with rotations
|
|
2
|
+
|
|
3
|
+
[](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
|
|
4
|
+
[](https://github.com/ddc/pythonLogs/blob/main/LICENSE)
|
|
5
|
+
[](https://pypi.python.org/pypi/pythonLogs)
|
|
6
|
+
[](https://pepy.tech/projects/pythonLogs)
|
|
7
|
+
[](https://codecov.io/gh/ddc/pythonLogs)
|
|
8
|
+
[](https://github.com/psf/black)
|
|
9
|
+
[](https://actions-badge.atrox.dev/ddc/pythonLogs/goto?ref=main)
|
|
10
|
+
[](https://www.python.org)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# Logs
|
|
15
|
+
+ Parameters for all classes are declared as OPTIONAL
|
|
16
|
+
+ If any [.env](./pythonLogs/.env.example) variable is omitted, it falls back to default values here: [settings.py](pythonLogs/settings.py)
|
|
17
|
+
+ Function arguments will overwrite any env variable
|
|
18
|
+
+ Timezone parameter can also accept `localtime`, default to `UTC`
|
|
19
|
+
+ This parameter is only to display the timezone datetime inside the log file
|
|
20
|
+
+ For timed rotation, only UTC and localtime are supported, meaning it will rotate at UTC or localtime
|
|
21
|
+
+ env variable to change between UTC and localtime is `LOG_ROTATE_AT_UTC` and default to True
|
|
22
|
+
+ Streamhandler parameter will add stream handler along with file handler
|
|
23
|
+
+ Showlocation parameter will show the filename and the line number where the message originated
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Install
|
|
29
|
+
```shell
|
|
30
|
+
pip install pythonLogs
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# BasicLog
|
|
36
|
+
+ Setup Logging
|
|
37
|
+
+ This is just a basic log, it does not use any file
|
|
38
|
+
```python
|
|
39
|
+
from pythonLogs import BasicLog
|
|
40
|
+
logger = BasicLog(
|
|
41
|
+
level="debug",
|
|
42
|
+
name="app",
|
|
43
|
+
timezone="America/Sao_Paulo",
|
|
44
|
+
showlocation=False,
|
|
45
|
+
).init()
|
|
46
|
+
logger.warning("This is a warning example")
|
|
47
|
+
```
|
|
48
|
+
#### Example of output
|
|
49
|
+
`[2024-10-08T19:08:56.918-0300]:[WARNING]:[app]:This is a warning example`
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# SizeRotatingLog
|
|
56
|
+
+ Setup Logging
|
|
57
|
+
+ Logs will rotate based on the file size using the `maxmbytes` variable
|
|
58
|
+
+ Rotated logs will have a sequence number starting from 1: `app.log_1.gz, app.log_2.gz`
|
|
59
|
+
+ Logs will be deleted based on the `daystokeep` variable, defaults to 30
|
|
60
|
+
```python
|
|
61
|
+
from pythonLogs import SizeRotatingLog
|
|
62
|
+
logger = SizeRotatingLog(
|
|
63
|
+
level="debug",
|
|
64
|
+
name="app",
|
|
65
|
+
directory="/app/logs",
|
|
66
|
+
filenames=["main.log", "app1.log"],
|
|
67
|
+
maxmbytes=5,
|
|
68
|
+
daystokeep=7,
|
|
69
|
+
timezone="America/Chicago",
|
|
70
|
+
streamhandler=True,
|
|
71
|
+
showlocation=False
|
|
72
|
+
).init()
|
|
73
|
+
logger.warning("This is a warning example")
|
|
74
|
+
```
|
|
75
|
+
#### Example of output
|
|
76
|
+
`[2024-10-08T19:08:56.918-0500]:[WARNING]:[app]:This is a warning example`
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# TimedRotatingLog
|
|
83
|
+
+ Setup Logging
|
|
84
|
+
+ Logs will rotate based on `when` variable to a `.gz` file, defaults to `midnight`
|
|
85
|
+
+ Rotated log will have the sufix variable on its name: `app_20240816.log.gz`
|
|
86
|
+
+ Logs will be deleted based on the `daystokeep` variable, defaults to 30
|
|
87
|
+
+ Current 'when' events supported:
|
|
88
|
+
+ midnight — roll over at midnight
|
|
89
|
+
+ W{0-6} - roll over on a certain day; 0 - Monday
|
|
90
|
+
```python
|
|
91
|
+
from pythonLogs import TimedRotatingLog
|
|
92
|
+
logger = TimedRotatingLog(
|
|
93
|
+
level="debug",
|
|
94
|
+
name="app",
|
|
95
|
+
directory="/app/logs",
|
|
96
|
+
filenames=["main.log", "app2.log"],
|
|
97
|
+
when="midnight",
|
|
98
|
+
daystokeep=7,
|
|
99
|
+
timezone="UTC",
|
|
100
|
+
streamhandler=True,
|
|
101
|
+
showlocation=False
|
|
102
|
+
).init()
|
|
103
|
+
logger.warning("This is a warning example")
|
|
104
|
+
```
|
|
105
|
+
#### Example of output
|
|
106
|
+
`[2024-10-08T19:08:56.918-0000]:[WARNING]:[app]:This is a warning example`
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
## Env Variables (Optional)
|
|
113
|
+
```
|
|
114
|
+
LOG_LEVEL=DEBUG
|
|
115
|
+
LOG_TIMEZONE=America/Chicago
|
|
116
|
+
LOG_ENCODING=UTF-8
|
|
117
|
+
LOG_APPNAME=app
|
|
118
|
+
LOG_FILENAME=app.log
|
|
119
|
+
LOG_DIRECTORY=/app/logs
|
|
120
|
+
LOG_DAYS_TO_KEEP=30
|
|
121
|
+
LOG_STREAM_HANDLER=True
|
|
122
|
+
LOG_SHOW_LOCATION=False
|
|
123
|
+
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
|
|
124
|
+
|
|
125
|
+
# SizeRotatingLog
|
|
126
|
+
LOG_MAX_FILE_SIZE_MB=10
|
|
127
|
+
|
|
128
|
+
# TimedRotatingLog
|
|
129
|
+
LOG_ROTATE_WHEN=midnight
|
|
130
|
+
LOG_ROTATE_AT_UTC=True
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Source Code
|
|
137
|
+
### Build
|
|
138
|
+
```shell
|
|
139
|
+
poetry build -f wheel
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Run Tests and Get Coverage Report using Poe
|
|
145
|
+
```shell
|
|
146
|
+
poetry update --with test
|
|
147
|
+
poe tests
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# License
|
|
153
|
+
Released under the [MIT License](LICENSE)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
# Buy me a cup of coffee
|
|
159
|
+
+ [GitHub Sponsor](https://github.com/sponsors/ddc)
|
|
160
|
+
+ [ko-fi](https://ko-fi.com/ddcsta)
|
|
161
|
+
+ [Paypal](https://www.paypal.com/ncp/payment/6G9Z78QHUD4RJ)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["poetry-core>=1.9.1"]
|
|
3
|
+
build-backend = "poetry.core.masonry.api"
|
|
4
|
+
|
|
5
|
+
[tool.poetry]
|
|
6
|
+
name = "pythonLogs"
|
|
7
|
+
version = "3.0.12"
|
|
8
|
+
description = "Easy logs with rotations"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
authors = ["Daniel Costa <danieldcsta@gmail.com>"]
|
|
12
|
+
maintainers = ["Daniel Costa"]
|
|
13
|
+
repository = "https://github.com/ddc/pythonLogs"
|
|
14
|
+
homepage = "https://pypi.org/project/pythonLogs"
|
|
15
|
+
packages = [{include = "pythonLogs"}]
|
|
16
|
+
package-mode = true
|
|
17
|
+
keywords = [
|
|
18
|
+
"python3", "python-3", "python",
|
|
19
|
+
"log", "logging", "logger",
|
|
20
|
+
"logutils", "log-utils", "pythonLogs"
|
|
21
|
+
]
|
|
22
|
+
classifiers = [
|
|
23
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
24
|
+
"Development Status :: 5 - Production/Stable",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
"Environment :: Other Environment",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Natural Language :: English",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.poetry.group.test]
|
|
35
|
+
optional = true
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
[tool.poetry.dependencies]
|
|
39
|
+
python = "^3.10"
|
|
40
|
+
pydantic-settings = "^2.7.1"
|
|
41
|
+
python-dotenv = "^1.0.1"
|
|
42
|
+
pytz = "^2024.2"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
[tool.poetry.group.test.dependencies]
|
|
46
|
+
coverage = "^7.6.10"
|
|
47
|
+
poethepoet = "^0.32.0"
|
|
48
|
+
pytest = "^8.3.4"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
[tool.coverage.run]
|
|
52
|
+
omit = [
|
|
53
|
+
"tests/*",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
[tool.poe.tasks]
|
|
58
|
+
_test = "coverage run -m pytest -v"
|
|
59
|
+
_coverage_report = "coverage report"
|
|
60
|
+
_coverage_xml = "coverage xml"
|
|
61
|
+
tests = ["_test", "_coverage_report", "_coverage_xml"]
|
|
62
|
+
test = ["tests"]
|
|
@@ -0,0 +1,17 @@
|
|
|
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_STREAM_HANDLER=True
|
|
9
|
+
LOG_SHOW_LOCATION=False
|
|
10
|
+
LOG_DATE_FORMAT=%Y-%m-%dT%H:%M:%S
|
|
11
|
+
|
|
12
|
+
# SizeRotatingLog
|
|
13
|
+
LOG_MAX_FILE_SIZE_MB=10
|
|
14
|
+
|
|
15
|
+
# TimedRotatingLog
|
|
16
|
+
LOG_ROTATE_WHEN=midnight
|
|
17
|
+
LOG_ROTATE_AT_UTC=True
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from importlib.metadata import version
|
|
3
|
+
from typing import Literal, NamedTuple
|
|
4
|
+
from .timed_rotating import TimedRotatingLog
|
|
5
|
+
from .size_rotating import SizeRotatingLog
|
|
6
|
+
from .basic_log import BasicLog
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
__all__ = (
|
|
10
|
+
"BasicLog",
|
|
11
|
+
"TimedRotatingLog",
|
|
12
|
+
"SizeRotatingLog",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__title__ = "pythonLogs"
|
|
16
|
+
__author__ = "Daniel Costa"
|
|
17
|
+
__email__ = "danieldcsta@gmail.com>"
|
|
18
|
+
__license__ = "MIT"
|
|
19
|
+
__copyright__ = "Copyright 2024-present ddc"
|
|
20
|
+
_req_python_version = (3, 10, 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 logging, NamedTuple, Literal, VersionInfo, version, _version, _req_python_version
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from pythonLogs.log_utils import get_format, get_level, get_timezone_function
|
|
5
|
+
from pythonLogs.settings import LogSettings
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class BasicLog:
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
level: Optional[str] = None,
|
|
12
|
+
name: Optional[str] = None,
|
|
13
|
+
encoding: Optional[str] = None,
|
|
14
|
+
datefmt: Optional[str] = None,
|
|
15
|
+
timezone: Optional[str] = None,
|
|
16
|
+
showlocation: Optional[bool] = None,
|
|
17
|
+
):
|
|
18
|
+
_settings = LogSettings()
|
|
19
|
+
self.level = get_level(level or _settings.level)
|
|
20
|
+
self.appname = name or _settings.appname
|
|
21
|
+
self.encoding = encoding or _settings.encoding
|
|
22
|
+
self.datefmt = datefmt or _settings.date_format
|
|
23
|
+
self.timezone = timezone or _settings.timezone
|
|
24
|
+
self.showlocation = showlocation or _settings.show_location
|
|
25
|
+
|
|
26
|
+
def init(self):
|
|
27
|
+
logger = logging.getLogger(self.appname)
|
|
28
|
+
logger.setLevel(self.level)
|
|
29
|
+
logging.Formatter.converter = get_timezone_function(self.timezone)
|
|
30
|
+
_format = get_format(self.showlocation, self.appname, self.timezone)
|
|
31
|
+
logging.basicConfig(datefmt=self.datefmt, encoding=self.encoding, format=_format)
|
|
32
|
+
return logger
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
import errno
|
|
3
|
+
import gzip
|
|
4
|
+
import logging.handlers
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from datetime import datetime, timedelta, timezone as dttz
|
|
10
|
+
from time import struct_time
|
|
11
|
+
from typing import Any, Callable
|
|
12
|
+
import pytz
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_stream_handler(
|
|
16
|
+
level: int,
|
|
17
|
+
formatter: logging.Formatter,
|
|
18
|
+
) -> logging.StreamHandler:
|
|
19
|
+
|
|
20
|
+
stream_hdlr = logging.StreamHandler()
|
|
21
|
+
stream_hdlr.setFormatter(formatter)
|
|
22
|
+
stream_hdlr.setLevel(level)
|
|
23
|
+
return stream_hdlr
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_logger_and_formatter(
|
|
27
|
+
name: str,
|
|
28
|
+
datefmt: str,
|
|
29
|
+
show_location: bool,
|
|
30
|
+
timezone: str,
|
|
31
|
+
) -> [logging.Logger, logging.Formatter]:
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(name)
|
|
34
|
+
for handler in logger.handlers[:]:
|
|
35
|
+
handler.close()
|
|
36
|
+
logger.removeHandler(handler)
|
|
37
|
+
|
|
38
|
+
formatt = get_format(show_location, name, timezone)
|
|
39
|
+
formatter = logging.Formatter(formatt, datefmt=datefmt)
|
|
40
|
+
formatter.converter = get_timezone_function(timezone)
|
|
41
|
+
return logger, formatter
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def check_filename_instance(filenames: list | tuple) -> None:
|
|
45
|
+
if not isinstance(filenames, list | tuple):
|
|
46
|
+
err_msg = f"Unable to parse filenames. Filename instance is not list or tuple. | {filenames}"
|
|
47
|
+
write_stderr(err_msg)
|
|
48
|
+
raise TypeError(err_msg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def check_directory_permissions(directory_path: str) -> None:
|
|
52
|
+
if os.path.isdir(directory_path) and not os.access(directory_path, os.W_OK | os.X_OK):
|
|
53
|
+
err_msg = f"Unable to access directory | {directory_path}"
|
|
54
|
+
write_stderr(err_msg)
|
|
55
|
+
raise PermissionError(err_msg)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
if not os.path.isdir(directory_path):
|
|
59
|
+
os.makedirs(directory_path, mode=0o755, exist_ok=True)
|
|
60
|
+
except PermissionError as e:
|
|
61
|
+
err_msg = f"Unable to create directory | {directory_path}"
|
|
62
|
+
write_stderr(f"{err_msg} | {repr(e)}")
|
|
63
|
+
raise PermissionError(err_msg)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def remove_old_logs(logs_dir: str, days_to_keep: int) -> None:
|
|
67
|
+
files_list = list_files(logs_dir, ends_with=".gz")
|
|
68
|
+
for file in files_list:
|
|
69
|
+
try:
|
|
70
|
+
if is_older_than_x_days(file, days_to_keep):
|
|
71
|
+
delete_file(file)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
write_stderr(f"Unable to delete {days_to_keep} days old logs | {file} | {repr(e)}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def list_files(directory: str, ends_with: str) -> tuple:
|
|
77
|
+
"""
|
|
78
|
+
List all files in the given directory
|
|
79
|
+
and returns them in a list sorted by creation time in ascending order
|
|
80
|
+
:param directory:
|
|
81
|
+
:param ends_with:
|
|
82
|
+
:return: tuple
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
result: list = []
|
|
87
|
+
if os.path.isdir(directory):
|
|
88
|
+
result: list = [os.path.join(directory, f) for f in os.listdir(directory) if f.lower().endswith(ends_with)]
|
|
89
|
+
result.sort(key=os.path.getmtime)
|
|
90
|
+
return tuple(result)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
write_stderr(repr(e))
|
|
93
|
+
raise e
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def delete_file(path: str) -> bool:
|
|
97
|
+
"""
|
|
98
|
+
Remove the given file and returns True if the file was successfully removed
|
|
99
|
+
:param path:
|
|
100
|
+
:return: True
|
|
101
|
+
"""
|
|
102
|
+
try:
|
|
103
|
+
if os.path.isfile(path):
|
|
104
|
+
os.remove(path)
|
|
105
|
+
elif os.path.exists(path):
|
|
106
|
+
shutil.rmtree(path)
|
|
107
|
+
else:
|
|
108
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
|
109
|
+
except OSError as e:
|
|
110
|
+
write_stderr(repr(e))
|
|
111
|
+
raise e
|
|
112
|
+
return True
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_older_than_x_days(path: str, days: int) -> bool:
|
|
116
|
+
"""
|
|
117
|
+
Check if a file or directory is older than the specified number of days
|
|
118
|
+
:param path:
|
|
119
|
+
:param days:
|
|
120
|
+
:return:
|
|
121
|
+
"""
|
|
122
|
+
|
|
123
|
+
if not os.path.exists(path):
|
|
124
|
+
raise FileNotFoundError(errno.ENOENT, os.strerror(errno.ENOENT), path)
|
|
125
|
+
|
|
126
|
+
try:
|
|
127
|
+
if int(days) in (0, 1):
|
|
128
|
+
cutoff_time = datetime.today()
|
|
129
|
+
else:
|
|
130
|
+
cutoff_time = datetime.today() - timedelta(days=int(days))
|
|
131
|
+
except ValueError as e:
|
|
132
|
+
write_stderr(repr(e))
|
|
133
|
+
raise e
|
|
134
|
+
|
|
135
|
+
file_timestamp = os.stat(path).st_mtime
|
|
136
|
+
file_time = datetime.fromtimestamp(file_timestamp)
|
|
137
|
+
|
|
138
|
+
if file_time < cutoff_time:
|
|
139
|
+
return True
|
|
140
|
+
return False
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def write_stderr(msg: str) -> None:
|
|
144
|
+
"""
|
|
145
|
+
Write msg to stderr
|
|
146
|
+
:param msg:
|
|
147
|
+
:return: None
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
obj = datetime.now(dttz.utc)
|
|
151
|
+
dt = obj.astimezone(pytz.timezone(os.getenv("LOG_TIMEZONE", "UTC")))
|
|
152
|
+
dt_timezone = dt.strftime("%Y-%m-%dT%H:%M:%S.%f:%z")
|
|
153
|
+
sys.stderr.write(f"[{dt_timezone}]:[ERROR]:{msg}\n")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_level(level: str) -> logging:
|
|
157
|
+
"""
|
|
158
|
+
Get logging level
|
|
159
|
+
:param level:
|
|
160
|
+
:return: level
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
if not isinstance(level, str):
|
|
164
|
+
write_stderr(f"Unable to get log level. Setting default level to: 'INFO' ({logging.INFO})")
|
|
165
|
+
return logging.INFO
|
|
166
|
+
|
|
167
|
+
match level.lower():
|
|
168
|
+
case "debug":
|
|
169
|
+
return logging.DEBUG
|
|
170
|
+
case "warning" | "warn":
|
|
171
|
+
return logging.WARNING
|
|
172
|
+
case "error":
|
|
173
|
+
return logging.ERROR
|
|
174
|
+
case "critical" | "crit":
|
|
175
|
+
return logging.CRITICAL
|
|
176
|
+
case _:
|
|
177
|
+
return logging.INFO
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def get_log_path(directory: str, filename: str) -> str:
|
|
181
|
+
"""
|
|
182
|
+
Get log file path
|
|
183
|
+
:param directory:
|
|
184
|
+
:param filename:
|
|
185
|
+
:return: path as str
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
log_file_path = str(os.path.join(directory, filename))
|
|
189
|
+
err_message = f"Unable to open log file for writing | {log_file_path}"
|
|
190
|
+
|
|
191
|
+
try:
|
|
192
|
+
open(log_file_path, "a+").close()
|
|
193
|
+
except PermissionError as e:
|
|
194
|
+
write_stderr(f"{err_message} | {repr(e)}")
|
|
195
|
+
raise PermissionError(err_message)
|
|
196
|
+
except FileNotFoundError as e:
|
|
197
|
+
write_stderr(f"{err_message} | {repr(e)}")
|
|
198
|
+
raise FileNotFoundError(err_message)
|
|
199
|
+
except OSError as e:
|
|
200
|
+
write_stderr(f"{err_message} | {repr(e)}")
|
|
201
|
+
raise e
|
|
202
|
+
|
|
203
|
+
return log_file_path
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def get_format(show_location: bool, name: str, timezone: str) -> str:
|
|
207
|
+
_debug_fmt = ""
|
|
208
|
+
_logger_name = ""
|
|
209
|
+
|
|
210
|
+
if name:
|
|
211
|
+
_logger_name = f"[{name}]:"
|
|
212
|
+
|
|
213
|
+
if show_location:
|
|
214
|
+
_debug_fmt = "[%(filename)s:%(funcName)s:%(lineno)d]:"
|
|
215
|
+
|
|
216
|
+
if timezone == "localtime":
|
|
217
|
+
utc_offset = time.strftime("%z")
|
|
218
|
+
else:
|
|
219
|
+
utc_offset = datetime.now(pytz.timezone(timezone)).strftime("%z")
|
|
220
|
+
|
|
221
|
+
fmt = f"[%(asctime)s.%(msecs)03d{utc_offset}]:[%(levelname)s]:{_logger_name}{_debug_fmt}%(message)s"
|
|
222
|
+
return fmt
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def gzip_file_with_sufix(file_path, sufix) -> str | None:
|
|
226
|
+
"""
|
|
227
|
+
gzip file
|
|
228
|
+
:param file_path:
|
|
229
|
+
:param sufix:
|
|
230
|
+
:return: bool
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
if os.path.isfile(file_path):
|
|
234
|
+
sfname, sext = os.path.splitext(file_path)
|
|
235
|
+
renamed_dst = f"{sfname}_{sufix}{sext}.gz"
|
|
236
|
+
|
|
237
|
+
try:
|
|
238
|
+
with open(file_path, "rb") as fin:
|
|
239
|
+
with gzip.open(renamed_dst, "wb") as fout:
|
|
240
|
+
fout.writelines(fin)
|
|
241
|
+
except Exception as e:
|
|
242
|
+
write_stderr(f"Unable to gzip log file | {file_path} | {repr(e)}")
|
|
243
|
+
raise e
|
|
244
|
+
|
|
245
|
+
try:
|
|
246
|
+
delete_file(file_path)
|
|
247
|
+
except OSError as e:
|
|
248
|
+
write_stderr(f"Unable to delete source log file | {file_path} | {repr(e)}")
|
|
249
|
+
raise e
|
|
250
|
+
|
|
251
|
+
return renamed_dst
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def get_timezone_function(
|
|
255
|
+
time_zone: str,
|
|
256
|
+
) -> Callable[[float | None, Any], struct_time] | Callable[[Any], struct_time]:
|
|
257
|
+
|
|
258
|
+
match time_zone.lower():
|
|
259
|
+
case "utc":
|
|
260
|
+
return time.gmtime
|
|
261
|
+
case "localtime":
|
|
262
|
+
return time.localtime
|
|
263
|
+
case _:
|
|
264
|
+
return lambda *args: datetime.now(tz=pytz.timezone(time_zone)).timetuple()
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from pydantic import Field
|
|
5
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class LogLevel(str, Enum):
|
|
10
|
+
"""log levels"""
|
|
11
|
+
|
|
12
|
+
CRITICAL = "CRITICAL"
|
|
13
|
+
CRIT = "CRIT"
|
|
14
|
+
ERROR = "ERROR"
|
|
15
|
+
WARNING = "WARNING"
|
|
16
|
+
WARN = "WARN"
|
|
17
|
+
INFO = "INFO"
|
|
18
|
+
DEBUG = "DEBUG"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LogSettings(BaseSettings):
|
|
22
|
+
"""If any ENV variable is omitted, it falls back to default values here"""
|
|
23
|
+
|
|
24
|
+
load_dotenv()
|
|
25
|
+
|
|
26
|
+
level: Optional[LogLevel] = Field(default=LogLevel.INFO)
|
|
27
|
+
appname: Optional[str] = Field(default="app")
|
|
28
|
+
directory: Optional[str] = Field(default="/app/logs")
|
|
29
|
+
filename: Optional[str] = Field(default="app.log")
|
|
30
|
+
encoding: Optional[str] = Field(default="UTF-8")
|
|
31
|
+
date_format: Optional[str] = Field(default="%Y-%m-%dT%H:%M:%S")
|
|
32
|
+
days_to_keep: Optional[int] = Field(default=30)
|
|
33
|
+
timezone: Optional[str] = Field(default="UTC")
|
|
34
|
+
stream_handler: Optional[bool] = Field(default=True)
|
|
35
|
+
show_location: Optional[bool] = Field(default=False)
|
|
36
|
+
|
|
37
|
+
# SizeRotatingLog
|
|
38
|
+
max_file_size_mb: Optional[int] = Field(default=10)
|
|
39
|
+
|
|
40
|
+
# TimedRotatingLog
|
|
41
|
+
rotate_when: Optional[str] = Field(default="midnight")
|
|
42
|
+
rotate_at_utc: Optional[bool] = Field(default=True)
|
|
43
|
+
rotate_file_sufix: Optional[str] = Field(default="%Y%m%d")
|
|
44
|
+
|
|
45
|
+
model_config = SettingsConfigDict(env_prefix="LOG_", env_file=".env", extra="allow")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
import logging.handlers
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pythonLogs.log_utils import (
|
|
6
|
+
check_directory_permissions,
|
|
7
|
+
check_filename_instance,
|
|
8
|
+
get_level,
|
|
9
|
+
get_log_path,
|
|
10
|
+
get_logger_and_formatter,
|
|
11
|
+
get_stream_handler,
|
|
12
|
+
gzip_file_with_sufix,
|
|
13
|
+
list_files,
|
|
14
|
+
remove_old_logs,
|
|
15
|
+
write_stderr,
|
|
16
|
+
)
|
|
17
|
+
from pythonLogs.settings import LogSettings
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SizeRotatingLog:
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
level: Optional[str] = None,
|
|
24
|
+
name: Optional[str] = None,
|
|
25
|
+
directory: Optional[str] = None,
|
|
26
|
+
filenames: Optional[list | tuple] = None,
|
|
27
|
+
maxmbytes: Optional[int] = None,
|
|
28
|
+
daystokeep: Optional[int] = None,
|
|
29
|
+
encoding: Optional[str] = None,
|
|
30
|
+
datefmt: Optional[str] = None,
|
|
31
|
+
timezone: Optional[str] = None,
|
|
32
|
+
streamhandler: Optional[bool] = None,
|
|
33
|
+
showlocation: Optional[bool] = None,
|
|
34
|
+
):
|
|
35
|
+
_settings = LogSettings()
|
|
36
|
+
self.level = get_level(level or _settings.level)
|
|
37
|
+
self.appname = name or _settings.appname
|
|
38
|
+
self.directory = directory or _settings.directory
|
|
39
|
+
self.filenames = filenames or (_settings.filename,)
|
|
40
|
+
self.maxmbytes = maxmbytes or _settings.max_file_size_mb
|
|
41
|
+
self.daystokeep = daystokeep or _settings.days_to_keep
|
|
42
|
+
self.encoding = encoding or _settings.encoding
|
|
43
|
+
self.datefmt = datefmt or _settings.date_format
|
|
44
|
+
self.timezone = timezone or _settings.timezone
|
|
45
|
+
self.streamhandler = streamhandler or _settings.stream_handler
|
|
46
|
+
self.showlocation = showlocation or _settings.show_location
|
|
47
|
+
|
|
48
|
+
def init(self):
|
|
49
|
+
check_filename_instance(self.filenames)
|
|
50
|
+
check_directory_permissions(self.directory)
|
|
51
|
+
|
|
52
|
+
logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
|
|
53
|
+
logger.setLevel(self.level)
|
|
54
|
+
|
|
55
|
+
for file in self.filenames:
|
|
56
|
+
log_file_path = get_log_path(self.directory, file)
|
|
57
|
+
|
|
58
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
59
|
+
filename=log_file_path,
|
|
60
|
+
mode="a",
|
|
61
|
+
maxBytes=self.maxmbytes * 1024 * 1024,
|
|
62
|
+
backupCount=self.daystokeep,
|
|
63
|
+
encoding=self.encoding,
|
|
64
|
+
delay=False,
|
|
65
|
+
errors=None,
|
|
66
|
+
)
|
|
67
|
+
file_handler.rotator = GZipRotatorSize(self.directory, self.daystokeep)
|
|
68
|
+
file_handler.setFormatter(formatter)
|
|
69
|
+
file_handler.setLevel(self.level)
|
|
70
|
+
logger.addHandler(file_handler)
|
|
71
|
+
|
|
72
|
+
if self.streamhandler:
|
|
73
|
+
stream_hdlr = get_stream_handler(self.level, formatter)
|
|
74
|
+
logger.addHandler(stream_hdlr)
|
|
75
|
+
|
|
76
|
+
return logger
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class GZipRotatorSize:
|
|
80
|
+
def __init__(self, dir_logs: str, daystokeep: int):
|
|
81
|
+
self.directory = dir_logs
|
|
82
|
+
self.daystokeep = daystokeep
|
|
83
|
+
|
|
84
|
+
def __call__(self, source: str, dest: str) -> None:
|
|
85
|
+
remove_old_logs(self.directory, self.daystokeep)
|
|
86
|
+
if os.path.isfile(source) and os.stat(source).st_size > 0:
|
|
87
|
+
source_filename, _ = os.path.basename(source).split(".")
|
|
88
|
+
new_file_number = self._get_new_file_number(self.directory, source_filename)
|
|
89
|
+
if os.path.isfile(source):
|
|
90
|
+
gzip_file_with_sufix(source, new_file_number)
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _get_new_file_number(directory, source_filename):
|
|
94
|
+
new_file_number = 1
|
|
95
|
+
previous_gz_files = list_files(directory, ends_with=".gz")
|
|
96
|
+
for gz_file in previous_gz_files:
|
|
97
|
+
if source_filename in gz_file:
|
|
98
|
+
try:
|
|
99
|
+
oldest_file_name = gz_file.split(".")[0].split("_")
|
|
100
|
+
if len(oldest_file_name) > 1:
|
|
101
|
+
new_file_number = int(oldest_file_name[1]) + 1
|
|
102
|
+
except ValueError as e:
|
|
103
|
+
write_stderr(f"Unable to get previous gz log file number | {gz_file} | {repr(e)}")
|
|
104
|
+
raise
|
|
105
|
+
return new_file_number
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
import logging.handlers
|
|
3
|
+
import os
|
|
4
|
+
from typing import Optional
|
|
5
|
+
from pythonLogs.log_utils import (
|
|
6
|
+
check_directory_permissions,
|
|
7
|
+
check_filename_instance,
|
|
8
|
+
get_level,
|
|
9
|
+
get_log_path,
|
|
10
|
+
get_logger_and_formatter,
|
|
11
|
+
get_stream_handler,
|
|
12
|
+
gzip_file_with_sufix,
|
|
13
|
+
remove_old_logs,
|
|
14
|
+
)
|
|
15
|
+
from pythonLogs.settings import LogSettings
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class TimedRotatingLog:
|
|
19
|
+
"""
|
|
20
|
+
Current 'rotating_when' events supported for TimedRotatingLogs:
|
|
21
|
+
midnight - roll over at midnight
|
|
22
|
+
W{0-6} - roll over on a certain day; 0 - Monday
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
level: Optional[str] = None,
|
|
28
|
+
name: Optional[str] = None,
|
|
29
|
+
directory: Optional[str] = None,
|
|
30
|
+
filenames: Optional[list | tuple] = None,
|
|
31
|
+
when: Optional[str] = None,
|
|
32
|
+
sufix: Optional[str] = None,
|
|
33
|
+
daystokeep: Optional[int] = None,
|
|
34
|
+
encoding: Optional[str] = None,
|
|
35
|
+
datefmt: Optional[str] = None,
|
|
36
|
+
timezone: Optional[str] = None,
|
|
37
|
+
streamhandler: Optional[bool] = None,
|
|
38
|
+
showlocation: Optional[bool] = None,
|
|
39
|
+
rotateatutc: Optional[bool] = None,
|
|
40
|
+
):
|
|
41
|
+
_settings = LogSettings()
|
|
42
|
+
self.level = get_level(level or _settings.level)
|
|
43
|
+
self.appname = name or _settings.appname
|
|
44
|
+
self.directory = directory or _settings.directory
|
|
45
|
+
self.filenames = filenames or (_settings.filename,)
|
|
46
|
+
self.when = when or _settings.rotate_when
|
|
47
|
+
self.sufix = sufix or _settings.rotate_file_sufix
|
|
48
|
+
self.daystokeep = daystokeep or _settings.days_to_keep
|
|
49
|
+
self.encoding = encoding or _settings.encoding
|
|
50
|
+
self.datefmt = datefmt or _settings.date_format
|
|
51
|
+
self.timezone = timezone or _settings.timezone
|
|
52
|
+
self.streamhandler = streamhandler or _settings.stream_handler
|
|
53
|
+
self.showlocation = showlocation or _settings.show_location
|
|
54
|
+
self.rotateatutc = rotateatutc or _settings.rotate_at_utc
|
|
55
|
+
|
|
56
|
+
def init(self):
|
|
57
|
+
check_filename_instance(self.filenames)
|
|
58
|
+
check_directory_permissions(self.directory)
|
|
59
|
+
|
|
60
|
+
logger, formatter = get_logger_and_formatter(self.appname, self.datefmt, self.showlocation, self.timezone)
|
|
61
|
+
logger.setLevel(self.level)
|
|
62
|
+
|
|
63
|
+
for file in self.filenames:
|
|
64
|
+
log_file_path = get_log_path(self.directory, file)
|
|
65
|
+
|
|
66
|
+
file_handler = logging.handlers.TimedRotatingFileHandler(
|
|
67
|
+
filename=log_file_path,
|
|
68
|
+
encoding=self.encoding,
|
|
69
|
+
when=self.when,
|
|
70
|
+
utc=self.rotateatutc,
|
|
71
|
+
backupCount=self.daystokeep,
|
|
72
|
+
)
|
|
73
|
+
file_handler.suffix = self.sufix
|
|
74
|
+
file_handler.rotator = GZipRotatorTimed(self.directory, self.daystokeep)
|
|
75
|
+
file_handler.setFormatter(formatter)
|
|
76
|
+
file_handler.setLevel(self.level)
|
|
77
|
+
logger.addHandler(file_handler)
|
|
78
|
+
|
|
79
|
+
if self.streamhandler:
|
|
80
|
+
stream_hdlr = get_stream_handler(self.level, formatter)
|
|
81
|
+
logger.addHandler(stream_hdlr)
|
|
82
|
+
|
|
83
|
+
return logger
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class GZipRotatorTimed:
|
|
87
|
+
def __init__(self, dir_logs: str, days_to_keep: int):
|
|
88
|
+
self.dir = dir_logs
|
|
89
|
+
self.days_to_keep = days_to_keep
|
|
90
|
+
|
|
91
|
+
def __call__(self, source: str, dest: str) -> None:
|
|
92
|
+
remove_old_logs(self.dir, self.days_to_keep)
|
|
93
|
+
sufix = os.path.splitext(dest)[1].replace(".", "")
|
|
94
|
+
gzip_file_with_sufix(source, sufix)
|