clean-logging 0.3.0__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.
- clean_logging/__init__.py +4 -0
- clean_logging/core/interfaces/log_repository_interface.py +21 -0
- clean_logging/infrastructure/__init__.py +0 -0
- clean_logging/infrastructure/loggin_setup.py +84 -0
- clean_logging/infrastructure/repositories/file_log_repository.py +153 -0
- clean_logging-0.3.0.dist-info/METADATA +22 -0
- clean_logging-0.3.0.dist-info/RECORD +9 -0
- clean_logging-0.3.0.dist-info/WHEEL +5 -0
- clean_logging-0.3.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ILogRepository(ABC):
|
|
7
|
+
@abstractmethod
|
|
8
|
+
def get_logs(self, limit: int = None, offset: int = None):
|
|
9
|
+
"""
|
|
10
|
+
Save or update a log.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
log: Log entity from the domain layer.
|
|
14
|
+
|
|
15
|
+
Returns:
|
|
16
|
+
T: The saved or updated log entity.
|
|
17
|
+
|
|
18
|
+
Raises:
|
|
19
|
+
ValueError: If required fields are missing.
|
|
20
|
+
"""
|
|
21
|
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
import coloredlogs
|
|
4
|
+
import queue
|
|
5
|
+
import threading
|
|
6
|
+
from clean_logging.core.interfaces.log_repository_interface import ILogRepository
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ایجاد یک کیو برای ذخیره موقت لاگها
|
|
10
|
+
log_queue = queue.Queue()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def setup_logging(log_repository: ILogRepository):
|
|
14
|
+
"""تنظیم Logger برای نمایش رنگی در ترمینال و ذخیره در دیتابیس."""
|
|
15
|
+
|
|
16
|
+
if log_repository is None:
|
|
17
|
+
raise ValueError("Log repository must be provided for logging setup.")
|
|
18
|
+
|
|
19
|
+
if not logging.getLogger().hasHandlers():
|
|
20
|
+
logger = logging.getLogger() # Logger اصلی
|
|
21
|
+
logger.setLevel(logging.DEBUG)
|
|
22
|
+
|
|
23
|
+
# نمایش لاگ با رنگ در ترمینال
|
|
24
|
+
coloredlogs.install(
|
|
25
|
+
level='DEBUG',
|
|
26
|
+
logger=logger,
|
|
27
|
+
fmt='%(asctime)s - %(name)s - %(levelname)s - %(funcName)s - %(message)s',
|
|
28
|
+
datefmt='%Y-%m-%d %H:%M:%S',
|
|
29
|
+
level_styles={
|
|
30
|
+
'debug': {'color': 'green'},
|
|
31
|
+
'info': {'color': 'blue'},
|
|
32
|
+
'warning': {'color': 'yellow'},
|
|
33
|
+
'error': {'color': 'red'},
|
|
34
|
+
'critical': {'color': 'red', 'bold': True},
|
|
35
|
+
},
|
|
36
|
+
field_styles={
|
|
37
|
+
'asctime': {'color': 'cyan'},
|
|
38
|
+
'name': {'color': 'white'},
|
|
39
|
+
'levelname': {'color': 'white', 'bold': True},
|
|
40
|
+
'funcName': {'color': 'magenta'},
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
import re
|
|
46
|
+
def remove_ansi_codes(text: str) -> str:
|
|
47
|
+
|
|
48
|
+
ansi_escape = re.compile(r'\x1b\[[0-?]*[ -/]*[@-~]')
|
|
49
|
+
return ansi_escape.sub('', text)
|
|
50
|
+
class QueueLogHandler(logging.Handler):
|
|
51
|
+
def emit(self, record):
|
|
52
|
+
try:
|
|
53
|
+
message = record.getMessage()
|
|
54
|
+
clean_message = remove_ansi_codes(message)
|
|
55
|
+
# اضافه کردن لاگ به کیو
|
|
56
|
+
log_repository.log_queue.put({
|
|
57
|
+
'level': record.levelname,
|
|
58
|
+
'message': clean_message,
|
|
59
|
+
'function_name': record.funcName,
|
|
60
|
+
'filename': record.pathname,
|
|
61
|
+
'lineno': record.lineno
|
|
62
|
+
})
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"[QueueLogHandler] خطا در افزودن به کیو: {str(e)}")
|
|
65
|
+
|
|
66
|
+
# اضافه کردن هندلر کیو
|
|
67
|
+
queue_handler = QueueLogHandler()
|
|
68
|
+
logger.addHandler(queue_handler)
|
|
69
|
+
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
70
|
+
#logging.getLogger("requests").setLevel(logging.WARNING)
|
|
71
|
+
logging.getLogger("charset_normalizer").setLevel(logging.WARNING)
|
|
72
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
73
|
+
|
|
74
|
+
class StaticFileFilter(logging.Filter):
|
|
75
|
+
def filter(self, record):
|
|
76
|
+
if record.name == 'werkzeug':
|
|
77
|
+
msg = record.getMessage()
|
|
78
|
+
if ' /static/' in msg: # فاصله قبل از /static/ برای جلوگیری از false positive
|
|
79
|
+
return False
|
|
80
|
+
return True
|
|
81
|
+
|
|
82
|
+
logging.getLogger('werkzeug').addFilter(StaticFileFilter())
|
|
83
|
+
# شروع ترد برای ذخیره لاگها از کیو به دیتابیس
|
|
84
|
+
log_repository.start_queue_processor()
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import queue
|
|
3
|
+
import re
|
|
4
|
+
import threading
|
|
5
|
+
import time
|
|
6
|
+
from datetime import datetime, timedelta, timezone
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileLogRepository:
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
log_dir: str = "files/logs",
|
|
14
|
+
retention_days: int = 3,
|
|
15
|
+
log_queue: Optional[queue.Queue] = None
|
|
16
|
+
):
|
|
17
|
+
self.log_dir = log_dir
|
|
18
|
+
self.retention_days = retention_days
|
|
19
|
+
self.log_queue = log_queue
|
|
20
|
+
self._stop_event = threading.Event()
|
|
21
|
+
self._lock = threading.Lock()
|
|
22
|
+
self._last_cleanup = datetime.now(timezone.utc)
|
|
23
|
+
|
|
24
|
+
os.makedirs(self.log_dir, exist_ok=True)
|
|
25
|
+
self.delete_old_logs()
|
|
26
|
+
|
|
27
|
+
def _should_cleanup(self) -> bool:
|
|
28
|
+
now = datetime.now(timezone.utc)
|
|
29
|
+
return (now - self._last_cleanup) >= timedelta(hours=24)
|
|
30
|
+
|
|
31
|
+
def _cleanup_if_needed(self):
|
|
32
|
+
if self._should_cleanup():
|
|
33
|
+
self.delete_old_logs()
|
|
34
|
+
self._last_cleanup = datetime.now(timezone.utc)
|
|
35
|
+
|
|
36
|
+
def _get_log_filename(self) -> str:
|
|
37
|
+
today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
|
|
38
|
+
return os.path.join(self.log_dir, f"app_{today}.log")
|
|
39
|
+
|
|
40
|
+
def _write_log(self, log_entry: dict):
|
|
41
|
+
"""نوشتن لاگ به صورت ساده: timestamp [LEVEL] message"""
|
|
42
|
+
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S")
|
|
43
|
+
level = log_entry.get("level", "INFO").upper()
|
|
44
|
+
message = log_entry.get("message", "")
|
|
45
|
+
|
|
46
|
+
# فرمت: 2025-04-05 14:30:22 [INFO] پیام...
|
|
47
|
+
log_line = f"{timestamp} [{level}] {message}\n"
|
|
48
|
+
|
|
49
|
+
log_file = self._get_log_filename()
|
|
50
|
+
try:
|
|
51
|
+
with self._lock:
|
|
52
|
+
with open(log_file, "a", encoding="utf-8") as f:
|
|
53
|
+
f.write(log_line)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
print(f"[FileLogRepository] خطا در نوشتن لاگ: {e}")
|
|
56
|
+
|
|
57
|
+
def start_queue_processor(self):
|
|
58
|
+
if not self.log_queue:
|
|
59
|
+
print("[FileLogRepository] کیو تعریف نشده است.")
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
def process_queue():
|
|
63
|
+
while not self._stop_event.is_set():
|
|
64
|
+
try:
|
|
65
|
+
log = self.log_queue.get(timeout=3)
|
|
66
|
+
self._write_log({
|
|
67
|
+
"level": log["level"],
|
|
68
|
+
"message": log["message"],
|
|
69
|
+
# سایر فیلدها (function_name و ...) نادیده گرفته میشوند
|
|
70
|
+
})
|
|
71
|
+
self.log_queue.task_done()
|
|
72
|
+
except queue.Empty:
|
|
73
|
+
pass
|
|
74
|
+
except Exception as e:
|
|
75
|
+
print(f"[FileQueueProcessor] خطا: {e}")
|
|
76
|
+
time.sleep(1)
|
|
77
|
+
|
|
78
|
+
self._cleanup_if_needed()
|
|
79
|
+
|
|
80
|
+
thread = threading.Thread(target=process_queue, daemon=True)
|
|
81
|
+
thread.start()
|
|
82
|
+
|
|
83
|
+
def stop_queue_processor(self):
|
|
84
|
+
self._stop_event.set()
|
|
85
|
+
|
|
86
|
+
def get_logs(self, limit: int = 20, offset: int = 0) -> list:
|
|
87
|
+
"""
|
|
88
|
+
خواندن لاگها و بازگرداندن به صورت لیستی از دیکشنریها:
|
|
89
|
+
[
|
|
90
|
+
{"timestamp": "2025-04-05 14:30:22", "level": "INFO", "message": "..."},
|
|
91
|
+
...
|
|
92
|
+
]
|
|
93
|
+
"""
|
|
94
|
+
logs = []
|
|
95
|
+
log_pattern = re.compile(r'^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) \[([A-Z]+)\] (.*)$')
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
log_files = []
|
|
99
|
+
for i in range(self.retention_days):
|
|
100
|
+
date_str = (datetime.now(timezone.utc).date() - timedelta(days=i)).strftime("%Y-%m-%d")
|
|
101
|
+
file_path = os.path.join(self.log_dir, f"app_{date_str}.log")
|
|
102
|
+
if os.path.exists(file_path):
|
|
103
|
+
log_files.append(file_path)
|
|
104
|
+
|
|
105
|
+
# خواندن از جدیدترین فایل به قدیمیترین
|
|
106
|
+
for log_file in log_files:
|
|
107
|
+
with open(log_file, "r", encoding="utf-8") as f:
|
|
108
|
+
lines = f.readlines()
|
|
109
|
+
# معکوس کردن خطوط هر فایل برای جدیدترین اول
|
|
110
|
+
for line in reversed(lines):
|
|
111
|
+
line = line.strip()
|
|
112
|
+
if not line:
|
|
113
|
+
continue
|
|
114
|
+
match = log_pattern.match(line)
|
|
115
|
+
if match:
|
|
116
|
+
timestamp, level, message = match.groups()
|
|
117
|
+
logs.append({
|
|
118
|
+
"timestamp": timestamp,
|
|
119
|
+
"level": level,
|
|
120
|
+
"message": message
|
|
121
|
+
})
|
|
122
|
+
# اگر خط فرمت استاندارد نداشت، نادیده گرفته میشود
|
|
123
|
+
|
|
124
|
+
return logs[offset:offset + limit]
|
|
125
|
+
|
|
126
|
+
except Exception as e:
|
|
127
|
+
print(f"[FileLogRepository] خطا در خواندن لاگها: {e}")
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
def delete_old_logs(self):
|
|
131
|
+
try:
|
|
132
|
+
cutoff = datetime.now(timezone.utc).date() - timedelta(days=self.retention_days)
|
|
133
|
+
for filename in os.listdir(self.log_dir):
|
|
134
|
+
if filename.startswith("app_") and filename.endswith(".log"):
|
|
135
|
+
try:
|
|
136
|
+
date_str = filename.replace("app_", "").replace(".log", "")
|
|
137
|
+
log_date = datetime.strptime(date_str, "%Y-%m-%d").date()
|
|
138
|
+
if log_date < cutoff:
|
|
139
|
+
os.remove(os.path.join(self.log_dir, filename))
|
|
140
|
+
except Exception:
|
|
141
|
+
continue
|
|
142
|
+
self._last_cleanup = datetime.now(timezone.utc)
|
|
143
|
+
except Exception as e:
|
|
144
|
+
print(f"[FileLogRepository] خطا در حذف فایلهای قدیمی: {e}")
|
|
145
|
+
|
|
146
|
+
def get_total_log_count(self) -> int:
|
|
147
|
+
# اگر نیاز به شمارش دقیق دارید، میتوانید خطوط را بشمارید
|
|
148
|
+
# ولی برای سادگی همچنان 100 برمیگردانیم
|
|
149
|
+
return 1000
|
|
150
|
+
|
|
151
|
+
def close_connection(self):
|
|
152
|
+
self.stop_queue_processor()
|
|
153
|
+
self.delete_old_logs()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: clean_logging
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: A clean, modular, and interface-driven logging utility for Clean Architecture projects
|
|
5
|
+
Author-email: qasem <qasemt@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/qasemt/clean_logging
|
|
8
|
+
Project-URL: Repository, https://github.com/qasemt/clean_logging
|
|
9
|
+
Project-URL: Documentation, https://github.com/qasemt/clean_logging#readme
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.8
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
20
|
+
Classifier: Topic :: System :: Logging
|
|
21
|
+
Requires-Python: >=3.8
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
clean_logging/__init__.py,sha256=TitWZSGyPrpKT42dDeCUHXjUoDaMikLt2pNLmK8_zSw,173
|
|
2
|
+
clean_logging/core/interfaces/log_repository_interface.py,sha256=kmD5ECSmAZp15fXMcsPgA84m3fER3J41WXtz0ddH0Uw,443
|
|
3
|
+
clean_logging/infrastructure/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
4
|
+
clean_logging/infrastructure/loggin_setup.py,sha256=V0_BgXoj3kGu4rewqKwntH4yyjferhRjuTlr5xfqLZw,3477
|
|
5
|
+
clean_logging/infrastructure/repositories/file_log_repository.py,sha256=g4QnyURa1_Y8UI-8J1nSQQZw__IN2bpaBWrev6OmVTY,6244
|
|
6
|
+
clean_logging-0.3.0.dist-info/METADATA,sha256=vfH1vlTkdiv5LulZ7rCESveX-GSSoilO7LfGz_f_NTw,1026
|
|
7
|
+
clean_logging-0.3.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
8
|
+
clean_logging-0.3.0.dist-info/top_level.txt,sha256=lyi6HJxeiSbpxFxX4npEWPZTBls4Qaq7dyzF7w1zqRs,14
|
|
9
|
+
clean_logging-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
clean_logging
|