fsvlog 0.3.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fsvlog/__init__.py +18 -0
- fsvlog/fsvlog.py +381 -0
- fsvlog-0.3.2.dist-info/METADATA +290 -0
- fsvlog-0.3.2.dist-info/RECORD +7 -0
- fsvlog-0.3.2.dist-info/WHEEL +5 -0
- fsvlog-0.3.2.dist-info/licenses/LICENSE.txt +21 -0
- fsvlog-0.3.2.dist-info/top_level.txt +1 -0
fsvlog/__init__.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
2
|
+
|
|
3
|
+
from typing import Tuple
|
|
4
|
+
from .fsvlog import FSVLog, Colors, create_logger
|
|
5
|
+
|
|
6
|
+
__all__: Tuple[str, ...] = (
|
|
7
|
+
"FSVLog",
|
|
8
|
+
"Colors",
|
|
9
|
+
"create_logger",
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
def __dir__() -> Tuple[str, ...]:
|
|
13
|
+
return __all__ + ("__version__", "__doc__")
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> object:
|
|
16
|
+
if name not in __all__:
|
|
17
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
18
|
+
return globals()[name]
|
fsvlog/fsvlog.py
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import inspect
|
|
4
|
+
import sys
|
|
5
|
+
import traceback
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Optional, Dict, Any, List
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Colors:
|
|
12
|
+
@staticmethod
|
|
13
|
+
def colorize(text: str, rgb: tuple) -> str:
|
|
14
|
+
r, g, b = rgb
|
|
15
|
+
r = max(0, min(255, r))
|
|
16
|
+
g = max(0, min(255, g))
|
|
17
|
+
b = max(0, min(255, b))
|
|
18
|
+
return f"\033[38;2;{r};{g};{b}m{text}\033[0m"
|
|
19
|
+
|
|
20
|
+
GREEN = (0, 255, 0)
|
|
21
|
+
RED = (255, 0, 0)
|
|
22
|
+
BLUE = (0, 0, 255)
|
|
23
|
+
YELLOW = (255, 255, 0)
|
|
24
|
+
CYAN = (0, 255, 255)
|
|
25
|
+
MAGENTA = (255, 0, 255)
|
|
26
|
+
WHITE = (255, 255, 255)
|
|
27
|
+
GRAY = (128, 128, 128)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
LOG_LEVELS = {
|
|
31
|
+
'DEBUG': 10,
|
|
32
|
+
'INFO': 20,
|
|
33
|
+
'WARNING': 30,
|
|
34
|
+
'ERROR': 40,
|
|
35
|
+
'FATAL': 50
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class FSVLog:
|
|
40
|
+
def __init__(self, log_folder_path: str = "Logs", max_logs: int = 10,
|
|
41
|
+
max_file_size_mb: int = 100, log_format: str = None,
|
|
42
|
+
time_format: str = None, filename_time_format: str = None,
|
|
43
|
+
file_only: bool = False, console_only: bool = False,
|
|
44
|
+
use_json: bool = False, log_level: str = "DEBUG",
|
|
45
|
+
enable_metrics: bool = False,
|
|
46
|
+
enable_global_exception_logging: bool = True,
|
|
47
|
+
exception_format: str = "compact"):
|
|
48
|
+
|
|
49
|
+
if not isinstance(log_folder_path, str) or log_folder_path == "":
|
|
50
|
+
raise ValueError("log_folder_path must be a non-empty string")
|
|
51
|
+
if not isinstance(max_logs, int) or max_logs <= 0:
|
|
52
|
+
raise ValueError("max_logs must be a positive integer")
|
|
53
|
+
if not isinstance(max_file_size_mb, int) or max_file_size_mb <= 0:
|
|
54
|
+
raise ValueError("max_file_size_mb must be a positive integer")
|
|
55
|
+
if not isinstance(file_only, bool):
|
|
56
|
+
raise ValueError("file_only must be a boolean")
|
|
57
|
+
if not isinstance(console_only, bool):
|
|
58
|
+
raise ValueError("console_only must be a boolean")
|
|
59
|
+
if not isinstance(use_json, bool):
|
|
60
|
+
raise ValueError("use_json must be a boolean")
|
|
61
|
+
if not isinstance(exception_format, str):
|
|
62
|
+
raise ValueError("exception_format must be a string")
|
|
63
|
+
if exception_format not in ["full", "compact", "minimal"]:
|
|
64
|
+
raise ValueError("exception_format must be 'full', 'compact', or 'minimal'")
|
|
65
|
+
if file_only and console_only:
|
|
66
|
+
raise ValueError("file_only and console_only cannot both be True")
|
|
67
|
+
|
|
68
|
+
self.log_folder_path = log_folder_path
|
|
69
|
+
self.max_logs = max_logs
|
|
70
|
+
self.max_file_size = max_file_size_mb * 1024 * 1024
|
|
71
|
+
self.log_format = log_format or "{timestamp} [{level}] {message}"
|
|
72
|
+
self.time_format = time_format or "%Y-%m-%dT%H:%M:%S.%f"
|
|
73
|
+
self.filename_time_format = filename_time_format or "%Y%m%d_%H%M%S_%f"
|
|
74
|
+
self.file_only = file_only
|
|
75
|
+
self.console_only = console_only
|
|
76
|
+
self.use_json = use_json
|
|
77
|
+
self.log_level = LOG_LEVELS.get(log_level.upper(), 20)
|
|
78
|
+
self.enable_metrics = enable_metrics
|
|
79
|
+
self.exception_format = exception_format
|
|
80
|
+
|
|
81
|
+
self._needs_caller_info = any(x in self.log_format for x in ['filename', 'lineno', 'funcname'])
|
|
82
|
+
|
|
83
|
+
self.colors = Colors()
|
|
84
|
+
self._current_log_file = None
|
|
85
|
+
self._bound_context = {}
|
|
86
|
+
self._original_excepthook = None
|
|
87
|
+
|
|
88
|
+
if enable_metrics:
|
|
89
|
+
self.metrics = defaultdict(int)
|
|
90
|
+
|
|
91
|
+
if not self.console_only:
|
|
92
|
+
os.makedirs(self.log_folder_path, exist_ok=True)
|
|
93
|
+
self._open_log_file()
|
|
94
|
+
self._manage_log_files()
|
|
95
|
+
|
|
96
|
+
if enable_global_exception_logging:
|
|
97
|
+
self.enable_global_exception_logging()
|
|
98
|
+
|
|
99
|
+
def _should_log(self, level: str) -> bool:
|
|
100
|
+
return LOG_LEVELS.get(level, 20) >= self.log_level
|
|
101
|
+
|
|
102
|
+
def _update_metrics(self, level: str):
|
|
103
|
+
if self.enable_metrics:
|
|
104
|
+
self.metrics[level] += 1
|
|
105
|
+
|
|
106
|
+
def _get_timestamp(self) -> str:
|
|
107
|
+
now = datetime.now()
|
|
108
|
+
if self.time_format == "%Y-%m-%dT%H:%M:%S.%f":
|
|
109
|
+
result = now.strftime(self.time_format)[:-3]
|
|
110
|
+
return result + "Z"
|
|
111
|
+
elif self.time_format.endswith(".%f"):
|
|
112
|
+
result = now.strftime(self.time_format)[:-3]
|
|
113
|
+
return result
|
|
114
|
+
else:
|
|
115
|
+
return now.strftime(self.time_format)
|
|
116
|
+
|
|
117
|
+
def _get_timestamp_for_filename(self) -> str:
|
|
118
|
+
now = datetime.now()
|
|
119
|
+
result = now.strftime(self.filename_time_format)
|
|
120
|
+
if "%f" in self.filename_time_format:
|
|
121
|
+
result = result[:-3]
|
|
122
|
+
return result
|
|
123
|
+
|
|
124
|
+
def _get_caller_info(self) -> Dict[str, Any]:
|
|
125
|
+
if not self._needs_caller_info:
|
|
126
|
+
return {}
|
|
127
|
+
try:
|
|
128
|
+
frame = inspect.currentframe()
|
|
129
|
+
for _ in range(3):
|
|
130
|
+
if frame:
|
|
131
|
+
frame = frame.f_back
|
|
132
|
+
if frame and frame.f_code.co_name in ['_log', 'info', 'error', 'warning', 'debug', 'fatal', 'custom',
|
|
133
|
+
'exception']:
|
|
134
|
+
frame = frame.f_back
|
|
135
|
+
if frame:
|
|
136
|
+
return {
|
|
137
|
+
'filename': os.path.basename(frame.f_code.co_filename),
|
|
138
|
+
'lineno': frame.f_lineno,
|
|
139
|
+
'funcname': frame.f_code.co_name
|
|
140
|
+
}
|
|
141
|
+
except Exception:
|
|
142
|
+
pass
|
|
143
|
+
return {'filename': 'unknown', 'lineno': 0, 'funcname': 'unknown'}
|
|
144
|
+
|
|
145
|
+
def _open_log_file(self):
|
|
146
|
+
filename = f"log_{self._get_timestamp_for_filename()}.log"
|
|
147
|
+
self.log_file_path = os.path.join(self.log_folder_path, filename)
|
|
148
|
+
self._current_log_file = open(self.log_file_path, "a", encoding="utf-8")
|
|
149
|
+
|
|
150
|
+
def _rotate_log_file(self):
|
|
151
|
+
if self._current_log_file:
|
|
152
|
+
self._current_log_file.close()
|
|
153
|
+
|
|
154
|
+
timestamp = self._get_timestamp_for_filename()
|
|
155
|
+
rotated_name = os.path.join(self.log_folder_path, f"log_{timestamp}.log")
|
|
156
|
+
os.rename(self.log_file_path, rotated_name)
|
|
157
|
+
self._open_log_file()
|
|
158
|
+
|
|
159
|
+
def _manage_log_files(self):
|
|
160
|
+
log_files = [
|
|
161
|
+
os.path.join(self.log_folder_path, f)
|
|
162
|
+
for f in os.listdir(self.log_folder_path)
|
|
163
|
+
if os.path.isfile(os.path.join(self.log_folder_path, f)) and f.endswith('.log')
|
|
164
|
+
]
|
|
165
|
+
log_files.sort(key=os.path.getctime)
|
|
166
|
+
while len(log_files) > self.max_logs:
|
|
167
|
+
try:
|
|
168
|
+
os.remove(log_files.pop(0))
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"Failed to remove old log file: {e}")
|
|
171
|
+
|
|
172
|
+
def _format_log_entry(self, message: str, level: str, extra: Dict[str, Any] = None) -> str:
|
|
173
|
+
timestamp = self._get_timestamp()
|
|
174
|
+
caller = self._get_caller_info()
|
|
175
|
+
context = {**self._bound_context}
|
|
176
|
+
if extra:
|
|
177
|
+
context.update(extra)
|
|
178
|
+
log_type = context.pop('type', '')
|
|
179
|
+
if self.use_json:
|
|
180
|
+
log_data = {
|
|
181
|
+
"timestamp": timestamp,
|
|
182
|
+
"level": level,
|
|
183
|
+
"message": message,
|
|
184
|
+
"pid": os.getpid(),
|
|
185
|
+
**context,
|
|
186
|
+
**caller
|
|
187
|
+
}
|
|
188
|
+
if log_type:
|
|
189
|
+
log_data['type'] = log_type
|
|
190
|
+
return json.dumps(log_data)
|
|
191
|
+
|
|
192
|
+
context_str = json.dumps(context, ensure_ascii=False) if context else ""
|
|
193
|
+
format_vars = {
|
|
194
|
+
'timestamp': timestamp,
|
|
195
|
+
'level': level,
|
|
196
|
+
'message': message,
|
|
197
|
+
'context': context_str,
|
|
198
|
+
'type': log_type,
|
|
199
|
+
**caller,
|
|
200
|
+
**context
|
|
201
|
+
}
|
|
202
|
+
try:
|
|
203
|
+
result = self.log_format.format(**format_vars)
|
|
204
|
+
if not log_type:
|
|
205
|
+
result = result.replace("[{type}]", "").replace("{type}", "").replace(" ", " ").replace("[]", "")
|
|
206
|
+
while " " in result:
|
|
207
|
+
result = result.replace(" ", " ")
|
|
208
|
+
if not context_str:
|
|
209
|
+
while " " in result:
|
|
210
|
+
result = result.replace(" ", " ")
|
|
211
|
+
|
|
212
|
+
return result.strip()
|
|
213
|
+
except KeyError as e:
|
|
214
|
+
return f"{timestamp} [{level}] ERROR: Invalid format key {e} - {message}"
|
|
215
|
+
|
|
216
|
+
def _write_to_file(self, log_entry: str):
|
|
217
|
+
if self.console_only:
|
|
218
|
+
return
|
|
219
|
+
try:
|
|
220
|
+
if os.path.getsize(self.log_file_path) > self.max_file_size:
|
|
221
|
+
self._rotate_log_file()
|
|
222
|
+
self._current_log_file.write(log_entry + "\n")
|
|
223
|
+
self._current_log_file.flush()
|
|
224
|
+
except Exception as e:
|
|
225
|
+
if not self.file_only:
|
|
226
|
+
error_msg = self._format_log_entry(f"Failed to write to log file: {e}", "ERROR")
|
|
227
|
+
print(self.colors.colorize(error_msg, Colors.RED))
|
|
228
|
+
|
|
229
|
+
def _log(self, message: str, level: str, color: tuple = None, extra: Dict[str, Any] = None):
|
|
230
|
+
if not self._should_log(level):
|
|
231
|
+
return None
|
|
232
|
+
self._update_metrics(level)
|
|
233
|
+
log_entry = self._format_log_entry(message, level, extra)
|
|
234
|
+
if not self.file_only:
|
|
235
|
+
if color:
|
|
236
|
+
print(self.colors.colorize(log_entry, color))
|
|
237
|
+
else:
|
|
238
|
+
print(log_entry)
|
|
239
|
+
if not self.console_only:
|
|
240
|
+
self._write_to_file(log_entry)
|
|
241
|
+
return log_entry
|
|
242
|
+
|
|
243
|
+
def _global_exception_handler(self, exc_type, exc_value, exc_traceback):
|
|
244
|
+
if exc_type is not KeyboardInterrupt:
|
|
245
|
+
filename = "unknown"
|
|
246
|
+
lineno = 0
|
|
247
|
+
funcname = "unknown"
|
|
248
|
+
if exc_traceback:
|
|
249
|
+
tb = exc_traceback
|
|
250
|
+
while tb.tb_next:
|
|
251
|
+
tb = tb.tb_next
|
|
252
|
+
frame = tb.tb_frame
|
|
253
|
+
filename = os.path.basename(frame.f_code.co_filename)
|
|
254
|
+
lineno = tb.tb_lineno
|
|
255
|
+
funcname = frame.f_code.co_name
|
|
256
|
+
|
|
257
|
+
if self.exception_format == "compact":
|
|
258
|
+
error_msg = f"Exception in {filename}:{lineno} ({funcname}): {exc_type.__name__}: {exc_value}"
|
|
259
|
+
elif self.exception_format == "minimal":
|
|
260
|
+
error_msg = f"{exc_type.__name__}: {exc_value} [{filename}:{lineno}]"
|
|
261
|
+
else:
|
|
262
|
+
tb_text = ''.join(traceback.format_exception(exc_type, exc_value, exc_traceback))
|
|
263
|
+
error_msg = f"Unhandled exception in [{filename}:{lineno}] ({funcname}):\n{tb_text}"
|
|
264
|
+
|
|
265
|
+
old_needs_caller = self._needs_caller_info
|
|
266
|
+
old_format = self.log_format
|
|
267
|
+
self._needs_caller_info = False
|
|
268
|
+
self.log_format = "{timestamp} [{level}] {message}"
|
|
269
|
+
self._log(error_msg, "FATAL", Colors.MAGENTA)
|
|
270
|
+
self._needs_caller_info = old_needs_caller
|
|
271
|
+
self.log_format = old_format
|
|
272
|
+
|
|
273
|
+
def enable_global_exception_logging(self):
|
|
274
|
+
self._original_excepthook = sys.excepthook
|
|
275
|
+
sys.excepthook = lambda *args: self._global_exception_handler(*args)
|
|
276
|
+
|
|
277
|
+
def set_exception_format(self, format: str):
|
|
278
|
+
if format not in ["full", "compact", "minimal"]:
|
|
279
|
+
raise ValueError("format must be 'full', 'compact', or 'minimal'")
|
|
280
|
+
self.exception_format = format
|
|
281
|
+
|
|
282
|
+
def bind(self, **kwargs) -> 'FSVLog':
|
|
283
|
+
new_logger = FSVLog(
|
|
284
|
+
log_folder_path=self.log_folder_path,
|
|
285
|
+
max_logs=self.max_logs,
|
|
286
|
+
max_file_size_mb=self.max_file_size // (1024 * 1024),
|
|
287
|
+
log_format=self.log_format,
|
|
288
|
+
time_format=self.time_format,
|
|
289
|
+
filename_time_format=self.filename_time_format,
|
|
290
|
+
file_only=self.file_only,
|
|
291
|
+
console_only=self.console_only,
|
|
292
|
+
use_json=self.use_json,
|
|
293
|
+
log_level=self._get_log_level_name(),
|
|
294
|
+
enable_metrics=self.enable_metrics,
|
|
295
|
+
exception_format=self.exception_format
|
|
296
|
+
)
|
|
297
|
+
new_logger._bound_context = {**self._bound_context, **kwargs}
|
|
298
|
+
return new_logger
|
|
299
|
+
|
|
300
|
+
def type(self, log_type: str) -> 'FSVLog':
|
|
301
|
+
return self.bind(type=log_type)
|
|
302
|
+
|
|
303
|
+
def _get_log_level_name(self) -> str:
|
|
304
|
+
for name, value in LOG_LEVELS.items():
|
|
305
|
+
if value == self.log_level:
|
|
306
|
+
return name
|
|
307
|
+
return "INFO"
|
|
308
|
+
|
|
309
|
+
def set_log_level(self, level: str):
|
|
310
|
+
self.log_level = LOG_LEVELS.get(level.upper(), 20)
|
|
311
|
+
|
|
312
|
+
def set_time_format(self, time_format: str):
|
|
313
|
+
self.time_format = time_format
|
|
314
|
+
|
|
315
|
+
def set_log_format(self, log_format: str):
|
|
316
|
+
self.log_format = log_format
|
|
317
|
+
self._needs_caller_info = any(x in log_format for x in ['filename', 'lineno', 'funcname'])
|
|
318
|
+
|
|
319
|
+
def exception(self, message: str, extra: Dict[str, Any] = None):
|
|
320
|
+
tb = traceback.format_exc()
|
|
321
|
+
if tb and tb != "NoneType: None\n":
|
|
322
|
+
exc_type, exc_value, exc_traceback = sys.exc_info()
|
|
323
|
+
filename = "unknown"
|
|
324
|
+
lineno = 0
|
|
325
|
+
funcname = "unknown"
|
|
326
|
+
if exc_traceback:
|
|
327
|
+
tb_obj = exc_traceback
|
|
328
|
+
while tb_obj.tb_next:
|
|
329
|
+
tb_obj = tb_obj.tb_next
|
|
330
|
+
frame = tb_obj.tb_frame
|
|
331
|
+
filename = os.path.basename(frame.f_code.co_filename)
|
|
332
|
+
lineno = tb_obj.tb_lineno
|
|
333
|
+
funcname = frame.f_code.co_name
|
|
334
|
+
if self.exception_format == "compact":
|
|
335
|
+
error_msg = f"{message}: {exc_type.__name__}: {exc_value} [{filename}:{lineno}]"
|
|
336
|
+
elif self.exception_format == "minimal":
|
|
337
|
+
error_msg = f"{message}: {exc_type.__name__}: {exc_value}"
|
|
338
|
+
else:
|
|
339
|
+
error_msg = f"{message}\n{tb}"
|
|
340
|
+
else:
|
|
341
|
+
error_msg = message
|
|
342
|
+
|
|
343
|
+
return self._log(error_msg, "ERROR", Colors.RED, extra)
|
|
344
|
+
|
|
345
|
+
def fatal(self, message: str, extra: Dict[str, Any] = None):
|
|
346
|
+
return self._log(message, "FATAL", Colors.MAGENTA, extra)
|
|
347
|
+
|
|
348
|
+
def error(self, message: str, extra: Dict[str, Any] = None):
|
|
349
|
+
return self._log(message, "ERROR", Colors.RED, extra)
|
|
350
|
+
|
|
351
|
+
def warning(self, message: str, extra: Dict[str, Any] = None):
|
|
352
|
+
return self._log(message, "WARNING", Colors.YELLOW, extra)
|
|
353
|
+
|
|
354
|
+
def info(self, message: str, extra: Dict[str, Any] = None):
|
|
355
|
+
return self._log(message, "INFO", Colors.GREEN, extra)
|
|
356
|
+
|
|
357
|
+
def debug(self, message: str, extra: Dict[str, Any] = None):
|
|
358
|
+
return self._log(message, "DEBUG", Colors.CYAN, extra)
|
|
359
|
+
|
|
360
|
+
def custom(self, message: str, level: str, color: tuple = None, extra: Dict[str, Any] = None):
|
|
361
|
+
return self._log(message, level.upper(), color, extra)
|
|
362
|
+
|
|
363
|
+
def get_current_log_file(self) -> str:
|
|
364
|
+
return self.log_file_path
|
|
365
|
+
|
|
366
|
+
def get_metrics(self) -> Dict[str, int]:
|
|
367
|
+
if not self.enable_metrics:
|
|
368
|
+
return {}
|
|
369
|
+
return dict(self.metrics)
|
|
370
|
+
|
|
371
|
+
def reset_metrics(self):
|
|
372
|
+
if self.enable_metrics:
|
|
373
|
+
self.metrics.clear()
|
|
374
|
+
|
|
375
|
+
def close(self):
|
|
376
|
+
if hasattr(self, '_current_log_file') and self._current_log_file:
|
|
377
|
+
self._current_log_file.close()
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def create_logger(log_folder_path: str = "Logs", max_logs: int = 10, **kwargs):
|
|
381
|
+
return FSVLog(log_folder_path, max_logs, **kwargs)
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fsvlog
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Advanced logging library with colored output, file rotation, and flexible configuration
|
|
5
|
+
Author-email: Fsainv028 <fsainv028@gmail.com>
|
|
6
|
+
Project-URL: Homepage, https://github.com/Fsainv028/FSVLog
|
|
7
|
+
Project-URL: Repository, https://github.com/Fsainv028/FSVLog.git
|
|
8
|
+
Project-URL: Release Notes, https://github.com/Fsainv028/FSVLog/releases
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Intended Audience :: Developers
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE.txt
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=6.0; extra == "dev"
|
|
26
|
+
Requires-Dist: black>=22.0; extra == "dev"
|
|
27
|
+
Dynamic: license-file
|
|
28
|
+
|
|
29
|
+
# FSVLog - Advanced Python Logging Library
|
|
30
|
+
> A powerful and flexible logging library with colored console output, file rotation, JSON support, metrics, and global exception handling.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
[](https://badge.fury.io/py/fsvlog)
|
|
34
|
+
[](https://pypi.org/project/fsvlog/)
|
|
35
|
+
[](https://opensource.org/licenses/MIT)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Features
|
|
39
|
+
- 🎨 Colored output with RGB color support for console logging
|
|
40
|
+
- 📁 File logging with automatic rotation and cleanup
|
|
41
|
+
- 🔄 Flexible modes - File-only, console-only, or both
|
|
42
|
+
- 📏 Size-based rotation to prevent log files from growing too large
|
|
43
|
+
- 🗑️ Automatic cleanup to keep only the newest log files
|
|
44
|
+
- ✨ Custom formatting with flexible log message templates
|
|
45
|
+
- 📊 Metrics tracking to count logs by level
|
|
46
|
+
- 🛡️ Global exception handling to catch and log unhandled exceptions
|
|
47
|
+
- 📋 JSON format support for machine-readable logs
|
|
48
|
+
- 🔗 Context binding to add persistent context to log entries
|
|
49
|
+
- 🎯 Multiple log levels: DEBUG, INFO, WARNING, ERROR, FATAL
|
|
50
|
+
- ⚡ Zero dependencies - pure Python, no external packages
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
## Installation
|
|
54
|
+
```bash
|
|
55
|
+
pip install fsvlog
|
|
56
|
+
```
|
|
57
|
+
---
|
|
58
|
+
## Quick Start
|
|
59
|
+
```python
|
|
60
|
+
from fsvlog import FSVLog
|
|
61
|
+
|
|
62
|
+
# Create logger with default settings
|
|
63
|
+
logger = FSVLog()
|
|
64
|
+
|
|
65
|
+
# Log messages with different levels
|
|
66
|
+
logger.debug("Debug message")
|
|
67
|
+
logger.info("Info message")
|
|
68
|
+
logger.warning("Warning message")
|
|
69
|
+
logger.error("Error message")
|
|
70
|
+
logger.fatal("Fatal message")
|
|
71
|
+
```
|
|
72
|
+
## Advanced Usage
|
|
73
|
+
### Basic Configuration
|
|
74
|
+
```python
|
|
75
|
+
logger = FSVLog(
|
|
76
|
+
log_folder_path="logs", # Log directory (default: "Logs")
|
|
77
|
+
max_logs=10, # Max log files to keep (default: 10)
|
|
78
|
+
max_file_size_mb=100, # Max file size before rotation (default: 100MB)
|
|
79
|
+
log_level="INFO", # Minimum log level (default: "DEBUG")
|
|
80
|
+
file_only=False, # Log only to file (default: False)
|
|
81
|
+
console_only=False, # Log only to console (default: False)
|
|
82
|
+
use_json=False, # Use JSON format (default: False)
|
|
83
|
+
enable_metrics=False, # Enable metrics tracking (default: False)
|
|
84
|
+
enable_global_exception_logging=True, # Catch unhandled exceptions
|
|
85
|
+
exception_format="compact" # "full", "compact", or "minimal"
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
### Custom Formatting
|
|
89
|
+
```python
|
|
90
|
+
# Custom log format
|
|
91
|
+
logger = FSVLog(
|
|
92
|
+
log_format="{timestamp} [{level}] [{filename}:{lineno}] {message}",
|
|
93
|
+
time_format="%Y-%m-%d %H:%M:%S"
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
logger.info("Custom format example")
|
|
97
|
+
# Output: 2026-07-29 14:30:00 [INFO] [app.py:42] Custom format example
|
|
98
|
+
```
|
|
99
|
+
### Context Binding
|
|
100
|
+
```python
|
|
101
|
+
# Bind context to all subsequent logs
|
|
102
|
+
logger = FSVLog()
|
|
103
|
+
user_logger = logger.bind(user_id=123, session="abc123")
|
|
104
|
+
user_logger.info("User logged in")
|
|
105
|
+
# Output includes user_id and session
|
|
106
|
+
|
|
107
|
+
# Type-specific logging
|
|
108
|
+
auth_logger = logger.type("AUTH")
|
|
109
|
+
auth_logger.info("Authentication successful")
|
|
110
|
+
# Output: [AUTH] Authentication successful
|
|
111
|
+
```
|
|
112
|
+
### JSON Logging
|
|
113
|
+
```python
|
|
114
|
+
# Enable JSON format for structured logging
|
|
115
|
+
logger = FSVLog(use_json=True)
|
|
116
|
+
logger.info("Structured log", extra={"user": "john", "action": "login"})
|
|
117
|
+
# Output: {"timestamp": "2026-07-29T14:30:00.123Z", "level": "INFO", "message": "Structured log", "user": "john", "action": "login"}
|
|
118
|
+
```
|
|
119
|
+
### Metrics
|
|
120
|
+
```python
|
|
121
|
+
# Enable metrics tracking
|
|
122
|
+
logger = FSVLog(enable_metrics=True)
|
|
123
|
+
logger.info("Message 1")
|
|
124
|
+
logger.error("Error 1")
|
|
125
|
+
logger.info("Message 2")
|
|
126
|
+
|
|
127
|
+
metrics = logger.get_metrics()
|
|
128
|
+
print(metrics) # {'INFO': 2, 'ERROR': 1}
|
|
129
|
+
|
|
130
|
+
logger.reset_metrics()
|
|
131
|
+
```
|
|
132
|
+
### Exception Handling
|
|
133
|
+
```python
|
|
134
|
+
logger = FSVLog(exception_format="compact")
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
result = 1 / 0
|
|
138
|
+
except Exception:
|
|
139
|
+
logger.exception("Division by zero occurred")
|
|
140
|
+
# Output: Division by zero occurred: ZeroDivisionError: division by zero [app.py:42]
|
|
141
|
+
```
|
|
142
|
+
### Global Exception Logging
|
|
143
|
+
```python
|
|
144
|
+
# All unhandled exceptions will be automatically logged
|
|
145
|
+
logger = FSVLog(enable_global_exception_logging=True)
|
|
146
|
+
|
|
147
|
+
# This will be caught and logged
|
|
148
|
+
def buggy_function():
|
|
149
|
+
raise RuntimeError("Something went wrong!")
|
|
150
|
+
|
|
151
|
+
buggy_function() # Automatically logged as FATAL
|
|
152
|
+
```
|
|
153
|
+
### Custom Colors
|
|
154
|
+
```python
|
|
155
|
+
# Use predefined colors
|
|
156
|
+
logger.info("Success!", color=Colors.GREEN)
|
|
157
|
+
logger.error("Danger!", color=Colors.RED)
|
|
158
|
+
|
|
159
|
+
# Custom RGB colors
|
|
160
|
+
logger.custom("Custom colored message", "CUSTOM", (255, 165, 0)) # Orange
|
|
161
|
+
```
|
|
162
|
+
### Log Levels
|
|
163
|
+
| Level | Method | Color | Use Case |
|
|
164
|
+
|----------|----------|----------|----------|
|
|
165
|
+
| DEBUG | .debug() | Cyan | Detailed debugging info |
|
|
166
|
+
| INFO | .info() | Green | General information |
|
|
167
|
+
| WARNING | .warning() | Yellow | Warning messages |
|
|
168
|
+
| ERROR | .error() | Red | Error conditions |
|
|
169
|
+
| FATAL | .fatal() | Magenta | Critical errors |
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
## API Reference
|
|
173
|
+
### Class: FSVLog
|
|
174
|
+
Parameters:
|
|
175
|
+
|
|
176
|
+
- log_folder_path (str): Path to log directory. Default: "Logs"
|
|
177
|
+
- max_logs (int): Maximum number of log files to keep. Default: 10
|
|
178
|
+
- max_file_size_mb (int): Maximum file size in MB before rotation. Default: 100
|
|
179
|
+
- log_format (str): Custom log format template. Default: "{timestamp} [{level}] {message}"
|
|
180
|
+
- time_format (str): Time format string. Default: "%Y-%m-%dT%H:%M:%S.%f"
|
|
181
|
+
- filename_time_format (str): Filename time format. Default: "%Y%m%d_%H%M%S_%f"
|
|
182
|
+
- file_only (bool): Log only to file. Default: False
|
|
183
|
+
- console_only (bool): Log only to console. Default: False
|
|
184
|
+
- use_json (bool): Use JSON format. Default: False
|
|
185
|
+
- log_level (str): Minimum log level. Default: "DEBUG"
|
|
186
|
+
- enable_metrics (bool): Enable metrics tracking. Default: False
|
|
187
|
+
- enable_global_exception_logging (bool): Catch unhandled exceptions. Default: True
|
|
188
|
+
- exception_format (str): Exception format: "full", "compact", or "minimal". Default: "compact"
|
|
189
|
+
|
|
190
|
+
Available format keys:
|
|
191
|
+
|
|
192
|
+
{timestamp} - Current timestamp
|
|
193
|
+
{level} - Log level
|
|
194
|
+
{message} - Log message
|
|
195
|
+
{filename} - Source file name
|
|
196
|
+
{lineno} - Line number
|
|
197
|
+
{funcname} - Function name
|
|
198
|
+
{type} - Custom log type
|
|
199
|
+
{context} - Bound context (JSON)
|
|
200
|
+
|
|
201
|
+
Methods:
|
|
202
|
+
|
|
203
|
+
debug(msg, extra=None) - Log debug message
|
|
204
|
+
info(msg, extra=None) - Log info message
|
|
205
|
+
warning(msg, extra=None) - Log warning message
|
|
206
|
+
error(msg, extra=None) - Log error message
|
|
207
|
+
fatal(msg, extra=None) - Log fatal message
|
|
208
|
+
exception(msg, extra=None) - Log exception with traceback
|
|
209
|
+
custom(msg, level, color=None, extra=None) - Log with custom level and color
|
|
210
|
+
bind(**kwargs) - Create logger with bound context
|
|
211
|
+
type(log_type) - Create logger with specific log type
|
|
212
|
+
set_log_level(level) - Change minimum log level
|
|
213
|
+
set_log_format(format) - Change log format
|
|
214
|
+
set_time_format(format) - Change time format
|
|
215
|
+
get_current_log_file() - Get current log file path
|
|
216
|
+
get_metrics() - Get metrics dictionary
|
|
217
|
+
reset_metrics() - Reset metrics
|
|
218
|
+
close() - Close log file
|
|
219
|
+
enable_global_exception_logging() - Enable global exception handler
|
|
220
|
+
set_exception_format(format) - Set exception format
|
|
221
|
+
|
|
222
|
+
### Helper Function: create_logger()
|
|
223
|
+
```python
|
|
224
|
+
from fsvlog import create_logger
|
|
225
|
+
|
|
226
|
+
logger = create_logger("logs", max_logs=5, log_level="INFO")
|
|
227
|
+
```
|
|
228
|
+
## Examples
|
|
229
|
+
### Web Application Logging
|
|
230
|
+
```python
|
|
231
|
+
from fsvlog import FSVLog
|
|
232
|
+
|
|
233
|
+
logger = FSVLog(log_folder_path="web_logs", enable_metrics=True)
|
|
234
|
+
|
|
235
|
+
# Bind request context
|
|
236
|
+
req_logger = logger.bind(request_id="req-123", user="alice")
|
|
237
|
+
req_logger.info("Request received")
|
|
238
|
+
req_logger.error("Database connection failed")
|
|
239
|
+
|
|
240
|
+
# Type-based logging
|
|
241
|
+
access_logger = logger.type("ACCESS")
|
|
242
|
+
access_logger.info("GET /api/users 200")
|
|
243
|
+
```
|
|
244
|
+
### Microservice Logging
|
|
245
|
+
```python
|
|
246
|
+
logger = FSVLog(
|
|
247
|
+
log_folder_path="/var/log/myapp",
|
|
248
|
+
use_json=True,
|
|
249
|
+
enable_global_exception_logging=True,
|
|
250
|
+
exception_format="full"
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
# Add service context
|
|
254
|
+
svc_logger = logger.bind(service="auth-service", version="1.2.3")
|
|
255
|
+
svc_logger.info("Service started")
|
|
256
|
+
```
|
|
257
|
+
### Development Debugging
|
|
258
|
+
```python
|
|
259
|
+
import time
|
|
260
|
+
from fsvlog import FSVLog
|
|
261
|
+
|
|
262
|
+
logger = FSVLog(
|
|
263
|
+
console_only=True, # Console only for development
|
|
264
|
+
log_level="DEBUG",
|
|
265
|
+
log_format="{timestamp} [{level}] [{funcname}()] {message}"
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
def process_data():
|
|
269
|
+
logger.debug("Processing data...")
|
|
270
|
+
# ... processing
|
|
271
|
+
logger.info("Data processed successfully")
|
|
272
|
+
|
|
273
|
+
process_data()
|
|
274
|
+
```
|
|
275
|
+
---
|
|
276
|
+
# License
|
|
277
|
+
**MIT License - see LICENSE file for details**
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
# Contributing
|
|
281
|
+
**Contributions are welcome! Please feel free to submit a Pull Request.**
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
## Support
|
|
285
|
+
- **GitHub**: [Fsainv028/FSVLog](https://github.com/Fsainv028/FSVLog)
|
|
286
|
+
- **Issues**: [Report a bug](https://github.com/Fsainv028/FSVLog/issues)
|
|
287
|
+
- **Email**: fsainv028@gmail.com
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
Made with ❤️ by Fsainv028
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
fsvlog/__init__.py,sha256=iTEqkpC7PSDQe7Au48e1kvEmJky-5Ka-siIWGWAcEyY,434
|
|
2
|
+
fsvlog/fsvlog.py,sha256=isA5EH2Z7mps29B_QvnaO2pUb8AYpKCIHUdMFUSWkto,14802
|
|
3
|
+
fsvlog-0.3.2.dist-info/licenses/LICENSE.txt,sha256=IB9EP7RwSoUPAMI3OLuqpdMA2qfXXwvSMg1ddLJFoMU,1091
|
|
4
|
+
fsvlog-0.3.2.dist-info/METADATA,sha256=VxpIQzp63w5J6UtccXPrcSko4gpJwt9tvl9RlQMUXVA,10073
|
|
5
|
+
fsvlog-0.3.2.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
|
6
|
+
fsvlog-0.3.2.dist-info/top_level.txt,sha256=ZcNecbCB2QfYomNWys6x8m7L4d7-nCGXHTl72cxs6Rs,7
|
|
7
|
+
fsvlog-0.3.2.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) [2026] [Fsainv028]
|
|
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 @@
|
|
|
1
|
+
fsvlog
|