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.
@@ -0,0 +1,4 @@
1
+ from .infrastructure.loggin_setup import setup_logging
2
+ from .core.interfaces.log_repository_interface import ILogRepository
3
+
4
+ __all__ = ["setup_logging", "ILogRepository"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ clean_logging