xmi-logger 0.0.7__py3-none-any.whl → 0.0.9__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.
- xmi_logger/__version__.py +2 -2
- xmi_logger/advanced_features.py +743 -579
- xmi_logger/xmi_logger.py +266 -178
- {xmi_logger-0.0.7.dist-info → xmi_logger-0.0.9.dist-info}/METADATA +151 -69
- xmi_logger-0.0.9.dist-info/RECORD +8 -0
- {xmi_logger-0.0.7.dist-info → xmi_logger-0.0.9.dist-info}/WHEEL +1 -1
- xmi_logger-0.0.7.dist-info/RECORD +0 -8
- {xmi_logger-0.0.7.dist-info → xmi_logger-0.0.9.dist-info}/top_level.txt +0 -0
xmi_logger/advanced_features.py
CHANGED
|
@@ -1,380 +1,501 @@
|
|
|
1
|
-
#!/usr/bin/env python
|
|
2
|
-
# -*- coding:utf-8 -*-
|
|
3
|
-
|
|
4
1
|
"""
|
|
5
|
-
XmiLogger
|
|
6
|
-
|
|
2
|
+
XmiLogger 高级功能模块(实用版)
|
|
3
|
+
|
|
4
|
+
该模块聚焦可直接集成到业务中的能力:
|
|
5
|
+
脱敏与可选加密、安全备份恢复、日志压缩归档、SQLite 结构化存储、日志处理管道、健康检查与轻量性能指标。
|
|
7
6
|
"""
|
|
8
7
|
|
|
9
|
-
import
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
10
|
import json
|
|
11
|
-
import time
|
|
12
|
-
import os
|
|
13
|
-
import sys
|
|
14
|
-
import threading
|
|
15
|
-
from datetime import datetime, timedelta
|
|
16
|
-
from typing import Dict, Any, List, Optional, Union, Callable
|
|
17
|
-
from functools import wraps
|
|
18
|
-
from collections import defaultdict, deque
|
|
19
11
|
import logging
|
|
20
|
-
import
|
|
21
|
-
import pickle
|
|
22
|
-
import zlib
|
|
23
|
-
import socket
|
|
24
|
-
import struct
|
|
25
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
12
|
+
import os
|
|
26
13
|
import queue
|
|
27
|
-
import weakref
|
|
28
|
-
import gc
|
|
29
|
-
import psutil
|
|
30
|
-
import signal
|
|
31
|
-
from contextlib import contextmanager
|
|
32
|
-
import uuid
|
|
33
|
-
import inspect
|
|
34
|
-
import traceback
|
|
35
|
-
from dataclasses import dataclass, field
|
|
36
|
-
from enum import Enum
|
|
37
14
|
import re
|
|
38
|
-
import sqlite3
|
|
39
|
-
from pathlib import Path
|
|
40
|
-
import tempfile
|
|
41
15
|
import shutil
|
|
42
|
-
import
|
|
16
|
+
import sqlite3
|
|
43
17
|
import tarfile
|
|
18
|
+
import threading
|
|
19
|
+
import time
|
|
44
20
|
import zipfile
|
|
45
|
-
import
|
|
46
|
-
import
|
|
47
|
-
import
|
|
48
|
-
import
|
|
49
|
-
import
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
21
|
+
from collections import defaultdict, deque
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
from enum import Enum
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Any, Callable, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple, Union
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
import psutil
|
|
29
|
+
except Exception:
|
|
30
|
+
psutil = None
|
|
31
|
+
|
|
32
|
+
_logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
55
35
|
class LogFilter(Enum):
|
|
56
|
-
"""日志过滤器类型"""
|
|
57
36
|
NONE = "none"
|
|
58
37
|
REGEX = "regex"
|
|
59
38
|
KEYWORD = "keyword"
|
|
60
|
-
PATTERN = "pattern"
|
|
61
39
|
CUSTOM = "custom"
|
|
62
40
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
self.
|
|
92
|
-
self.
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
41
|
+
|
|
42
|
+
def _now_iso() -> str:
|
|
43
|
+
return datetime.now().isoformat(timespec="seconds")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _ensure_dir(path: Union[str, Path]) -> str:
|
|
47
|
+
p = Path(path)
|
|
48
|
+
p.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
return str(p)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _is_within_directory(base_dir: Path, target_path: Path) -> bool:
|
|
53
|
+
try:
|
|
54
|
+
base = base_dir.resolve()
|
|
55
|
+
target = target_path.resolve()
|
|
56
|
+
return str(target).startswith(str(base) + os.sep) or target == base
|
|
57
|
+
except Exception:
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class LogSecurity:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
sensitive_keys: Optional[Sequence[str]] = None,
|
|
65
|
+
replacement: str = "***",
|
|
66
|
+
enable_encryption: bool = False,
|
|
67
|
+
encryption_key: Optional[Union[str, bytes]] = None,
|
|
68
|
+
):
|
|
69
|
+
self.replacement = replacement
|
|
70
|
+
self.sensitive_keys = {k.lower() for k in (sensitive_keys or self._default_sensitive_keys())}
|
|
71
|
+
self._patterns = self._compile_patterns()
|
|
72
|
+
|
|
73
|
+
self._cipher = None
|
|
74
|
+
self._encryption_key: Optional[bytes] = None
|
|
75
|
+
if enable_encryption or encryption_key is not None:
|
|
76
|
+
self._init_cipher(encryption_key)
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _default_sensitive_keys() -> List[str]:
|
|
80
|
+
return [
|
|
81
|
+
"password",
|
|
82
|
+
"passwd",
|
|
83
|
+
"pwd",
|
|
84
|
+
"密码",
|
|
85
|
+
"口令",
|
|
86
|
+
"secret",
|
|
87
|
+
"token",
|
|
88
|
+
"api_key",
|
|
89
|
+
"apikey",
|
|
90
|
+
"密钥",
|
|
91
|
+
"access_token",
|
|
92
|
+
"refresh_token",
|
|
93
|
+
"private_key",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
def _compile_patterns(self) -> List[re.Pattern[str]]:
|
|
97
|
+
keys = sorted({re.escape(k) for k in self.sensitive_keys}, key=len, reverse=True)
|
|
98
|
+
if not keys:
|
|
97
99
|
return []
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
100
|
+
key_alt = "|".join(keys)
|
|
101
|
+
return [
|
|
102
|
+
re.compile(
|
|
103
|
+
rf'(?i)((?:{key_alt}))(\s*[:=:]\s*)(["\']?)([^"\',\s\}}\]\n\r]+)(\3)',
|
|
104
|
+
re.IGNORECASE,
|
|
105
|
+
),
|
|
106
|
+
re.compile(
|
|
107
|
+
rf'(?i)("?(?:{key_alt})"?)\s*:\s*(["\'])(.*?)\2',
|
|
108
|
+
re.IGNORECASE,
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def sanitize_message(self, message: str) -> str:
|
|
113
|
+
sanitized = message
|
|
114
|
+
|
|
115
|
+
for pattern in self._patterns:
|
|
116
|
+
def _repl(m: re.Match[str]) -> str:
|
|
117
|
+
if len(m.groups()) >= 5:
|
|
118
|
+
key = m.group(1)
|
|
119
|
+
sep = m.group(2)
|
|
120
|
+
quote = m.group(3) or ""
|
|
121
|
+
end_quote = m.group(5) or quote
|
|
122
|
+
return f"{key}{sep}{quote}{self.replacement}{end_quote}"
|
|
123
|
+
key = m.group(1)
|
|
124
|
+
quote = m.group(2)
|
|
125
|
+
return f"{key}: {quote}{self.replacement}{quote}"
|
|
126
|
+
|
|
127
|
+
sanitized = pattern.sub(_repl, sanitized)
|
|
128
|
+
|
|
129
|
+
return sanitized
|
|
130
|
+
|
|
131
|
+
def sanitize_mapping(self, data: Any) -> Any:
|
|
132
|
+
if isinstance(data, Mapping):
|
|
133
|
+
out: Dict[str, Any] = {}
|
|
134
|
+
for k, v in data.items():
|
|
135
|
+
key_str = str(k)
|
|
136
|
+
if key_str.lower() in self.sensitive_keys:
|
|
137
|
+
out[key_str] = self.replacement
|
|
138
|
+
else:
|
|
139
|
+
out[key_str] = self.sanitize_mapping(v)
|
|
140
|
+
return out
|
|
141
|
+
if isinstance(data, list):
|
|
142
|
+
return [self.sanitize_mapping(x) for x in data]
|
|
143
|
+
if isinstance(data, tuple):
|
|
144
|
+
return tuple(self.sanitize_mapping(x) for x in data)
|
|
145
|
+
return data
|
|
146
|
+
|
|
147
|
+
def _init_cipher(self, encryption_key: Optional[Union[str, bytes]]) -> None:
|
|
148
|
+
try:
|
|
149
|
+
from cryptography.fernet import Fernet
|
|
150
|
+
except Exception as e:
|
|
151
|
+
raise RuntimeError("cryptography 未安装,无法启用加密功能") from e
|
|
152
|
+
|
|
153
|
+
if encryption_key is None:
|
|
154
|
+
self._encryption_key = Fernet.generate_key()
|
|
155
|
+
elif isinstance(encryption_key, bytes):
|
|
156
|
+
self._encryption_key = encryption_key
|
|
157
|
+
else:
|
|
158
|
+
self._encryption_key = encryption_key.encode("utf-8")
|
|
159
|
+
|
|
160
|
+
self._cipher = Fernet(self._encryption_key)
|
|
161
|
+
|
|
162
|
+
def get_encryption_key(self) -> Optional[bytes]:
|
|
163
|
+
return self._encryption_key
|
|
164
|
+
|
|
165
|
+
def encrypt_bytes(self, data: bytes) -> bytes:
|
|
166
|
+
if self._cipher is None:
|
|
167
|
+
raise RuntimeError("加密未启用")
|
|
168
|
+
return self._cipher.encrypt(data)
|
|
169
|
+
|
|
170
|
+
def decrypt_bytes(self, data: bytes) -> bytes:
|
|
171
|
+
if self._cipher is None:
|
|
172
|
+
raise RuntimeError("加密未启用")
|
|
173
|
+
return self._cipher.decrypt(data)
|
|
135
174
|
|
|
136
|
-
# 新增:实时监控和性能分析
|
|
137
|
-
class PerformanceMonitor:
|
|
138
|
-
"""性能监控器"""
|
|
139
|
-
def __init__(self):
|
|
140
|
-
self.metrics = {
|
|
141
|
-
'log_count': 0,
|
|
142
|
-
'error_count': 0,
|
|
143
|
-
'avg_processing_time': 0.0,
|
|
144
|
-
'memory_usage': 0.0,
|
|
145
|
-
'cpu_usage': 0.0,
|
|
146
|
-
'throughput': 0.0
|
|
147
|
-
}
|
|
148
|
-
self.processing_times = deque(maxlen=1000)
|
|
149
|
-
self.start_time = time.time()
|
|
150
|
-
self.lock = threading.Lock()
|
|
151
|
-
self._monitor_thread = threading.Thread(target=self._monitor_worker, daemon=True)
|
|
152
|
-
self._monitor_thread.start()
|
|
153
|
-
|
|
154
|
-
def record_log(self, level: str, processing_time: float) -> None:
|
|
155
|
-
"""记录日志处理"""
|
|
156
|
-
with self.lock:
|
|
157
|
-
self.metrics['log_count'] += 1
|
|
158
|
-
if level.upper() == 'ERROR':
|
|
159
|
-
self.metrics['error_count'] += 1
|
|
160
|
-
|
|
161
|
-
self.processing_times.append(processing_time)
|
|
162
|
-
if self.processing_times:
|
|
163
|
-
self.metrics['avg_processing_time'] = sum(self.processing_times) / len(self.processing_times)
|
|
164
|
-
|
|
165
|
-
def _monitor_worker(self) -> None:
|
|
166
|
-
"""监控工作线程"""
|
|
167
|
-
while True:
|
|
168
|
-
try:
|
|
169
|
-
# 监控系统资源
|
|
170
|
-
process = psutil.Process()
|
|
171
|
-
self.metrics['memory_usage'] = process.memory_info().rss / 1024 / 1024 # MB
|
|
172
|
-
self.metrics['cpu_usage'] = process.cpu_percent()
|
|
173
|
-
|
|
174
|
-
# 计算吞吐量
|
|
175
|
-
elapsed = time.time() - self.start_time
|
|
176
|
-
if elapsed > 0:
|
|
177
|
-
self.metrics['throughput'] = self.metrics['log_count'] / elapsed
|
|
178
|
-
|
|
179
|
-
time.sleep(5) # 每5秒更新一次
|
|
180
|
-
except Exception:
|
|
181
|
-
time.sleep(5)
|
|
182
|
-
|
|
183
|
-
def get_metrics(self) -> Dict[str, Any]:
|
|
184
|
-
"""获取性能指标"""
|
|
185
|
-
with self.lock:
|
|
186
|
-
return self.metrics.copy()
|
|
187
175
|
|
|
188
|
-
# 新增:分布式日志支持
|
|
189
176
|
class DistributedLogger:
|
|
190
|
-
|
|
191
|
-
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
node_id: str,
|
|
180
|
+
sequence_dir: Optional[str] = None,
|
|
181
|
+
persist_every: int = 100,
|
|
182
|
+
):
|
|
192
183
|
self.node_id = node_id
|
|
193
|
-
self.
|
|
194
|
-
self.
|
|
195
|
-
self.
|
|
196
|
-
|
|
184
|
+
self.persist_every = max(1, int(persist_every))
|
|
185
|
+
self._lock = threading.Lock()
|
|
186
|
+
self._sequence_number = 0
|
|
187
|
+
|
|
188
|
+
base_dir = Path(sequence_dir) if sequence_dir else Path(os.getenv("XMI_LOGGER_SEQ_DIR", ""))
|
|
189
|
+
if not str(base_dir):
|
|
190
|
+
base_dir = Path(os.path.expanduser("~")) / ".xmi_logger"
|
|
191
|
+
_ensure_dir(base_dir)
|
|
192
|
+
self._sequence_file = str(base_dir / f"sequence_{self.node_id}.txt")
|
|
197
193
|
self._load_sequence()
|
|
198
|
-
|
|
194
|
+
|
|
199
195
|
def _load_sequence(self) -> None:
|
|
200
|
-
"""加载序列号"""
|
|
201
196
|
try:
|
|
202
197
|
if os.path.exists(self._sequence_file):
|
|
203
|
-
with open(self._sequence_file,
|
|
204
|
-
|
|
198
|
+
with open(self._sequence_file, "r", encoding="utf-8") as f:
|
|
199
|
+
value = f.read().strip()
|
|
200
|
+
self._sequence_number = int(value) if value else 0
|
|
205
201
|
except Exception:
|
|
206
|
-
self.
|
|
207
|
-
|
|
202
|
+
self._sequence_number = 0
|
|
203
|
+
|
|
208
204
|
def _save_sequence(self) -> None:
|
|
209
|
-
""
|
|
205
|
+
tmp = f"{self._sequence_file}.tmp"
|
|
210
206
|
try:
|
|
211
|
-
with open(
|
|
212
|
-
f.write(str(self.
|
|
207
|
+
with open(tmp, "w", encoding="utf-8") as f:
|
|
208
|
+
f.write(str(self._sequence_number))
|
|
209
|
+
os.replace(tmp, self._sequence_file)
|
|
213
210
|
except Exception:
|
|
214
|
-
|
|
215
|
-
|
|
211
|
+
try:
|
|
212
|
+
if os.path.exists(tmp):
|
|
213
|
+
os.remove(tmp)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
|
|
216
217
|
def get_log_id(self) -> str:
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
self.
|
|
218
|
+
with self._lock:
|
|
219
|
+
self._sequence_number += 1
|
|
220
|
+
if self._sequence_number % self.persist_every == 0:
|
|
221
|
+
self._save_sequence()
|
|
222
|
+
ts_ms = int(time.time() * 1000)
|
|
223
|
+
return f"{self.node_id}_{ts_ms}_{self._sequence_number}"
|
|
224
|
+
|
|
225
|
+
def flush(self) -> None:
|
|
226
|
+
with self._lock:
|
|
220
227
|
self._save_sequence()
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
self.
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
""
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
self.
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
class LogRouter:
|
|
259
|
-
"""智能日志路由器"""
|
|
260
|
-
def __init__(self):
|
|
261
|
-
self.routes = {}
|
|
262
|
-
self.default_route = None
|
|
263
|
-
self.lock = threading.Lock()
|
|
264
|
-
|
|
265
|
-
def add_route(self, condition: Callable, handler: Callable) -> None:
|
|
266
|
-
"""添加路由规则"""
|
|
267
|
-
with self.lock:
|
|
268
|
-
route_id = len(self.routes)
|
|
269
|
-
self.routes[route_id] = (condition, handler)
|
|
270
|
-
|
|
271
|
-
def set_default_route(self, handler: Callable) -> None:
|
|
272
|
-
"""设置默认路由"""
|
|
273
|
-
self.default_route = handler
|
|
274
|
-
|
|
275
|
-
def route_log(self, log_entry: Dict[str, Any]) -> None:
|
|
276
|
-
"""路由日志"""
|
|
277
|
-
with self.lock:
|
|
278
|
-
for route_id, (condition, handler) in self.routes.items():
|
|
279
|
-
if condition(log_entry):
|
|
280
|
-
handler(log_entry)
|
|
281
|
-
return
|
|
282
|
-
|
|
283
|
-
if self.default_route:
|
|
284
|
-
self.default_route(log_entry)
|
|
285
|
-
|
|
286
|
-
# 新增:日志加密和安全
|
|
287
|
-
class LogSecurity:
|
|
288
|
-
"""日志安全模块"""
|
|
289
|
-
def __init__(self, encryption_key: str = None):
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
class LogAggregator:
|
|
231
|
+
def __init__(
|
|
232
|
+
self,
|
|
233
|
+
window_size: int = 200,
|
|
234
|
+
flush_interval: float = 5.0,
|
|
235
|
+
key_fn: Optional[Callable[[Dict[str, Any]], str]] = None,
|
|
236
|
+
on_flush: Optional[Callable[[List[Dict[str, Any]]], None]] = None,
|
|
237
|
+
include_samples: bool = True,
|
|
238
|
+
):
|
|
239
|
+
self.window_size = max(1, int(window_size))
|
|
240
|
+
self.flush_interval = max(0.1, float(flush_interval))
|
|
241
|
+
self._key_fn = key_fn or (lambda e: f"{e.get('level', 'INFO')}:{str(e.get('message', ''))[:80]}")
|
|
242
|
+
self._on_flush = on_flush
|
|
243
|
+
self._include_samples = include_samples
|
|
244
|
+
|
|
245
|
+
self._buffer: deque[Dict[str, Any]] = deque(maxlen=self.window_size)
|
|
246
|
+
self._lock = threading.Lock()
|
|
247
|
+
self._stop = threading.Event()
|
|
248
|
+
self._output: "queue.Queue[List[Dict[str, Any]]]" = queue.Queue(maxsize=10)
|
|
249
|
+
self._last_flush_at = time.time()
|
|
250
|
+
|
|
251
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
252
|
+
self._thread.start()
|
|
253
|
+
|
|
254
|
+
def add_log(self, log_entry: Dict[str, Any]) -> None:
|
|
255
|
+
with self._lock:
|
|
256
|
+
self._buffer.append(log_entry)
|
|
257
|
+
if len(self._buffer) >= self.window_size:
|
|
258
|
+
self._flush_locked()
|
|
259
|
+
|
|
260
|
+
def flush(self) -> List[Dict[str, Any]]:
|
|
261
|
+
with self._lock:
|
|
262
|
+
return self._flush_locked()
|
|
263
|
+
|
|
264
|
+
def get_aggregated(self, timeout: float = 0.0) -> Optional[List[Dict[str, Any]]]:
|
|
290
265
|
try:
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
266
|
+
return self._output.get(timeout=timeout)
|
|
267
|
+
except queue.Empty:
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
def stop(self) -> None:
|
|
271
|
+
self._stop.set()
|
|
272
|
+
self._thread.join(timeout=2)
|
|
273
|
+
try:
|
|
274
|
+
self.flush()
|
|
275
|
+
except Exception:
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
def _flush_locked(self) -> List[Dict[str, Any]]:
|
|
279
|
+
if not self._buffer:
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
groups: Dict[str, List[Dict[str, Any]]] = defaultdict(list)
|
|
283
|
+
for entry in self._buffer:
|
|
284
|
+
groups[self._key_fn(entry)].append(entry)
|
|
285
|
+
|
|
286
|
+
aggregated: List[Dict[str, Any]] = []
|
|
287
|
+
for _, entries in groups.items():
|
|
288
|
+
if len(entries) == 1:
|
|
289
|
+
aggregated.append(entries[0])
|
|
290
|
+
continue
|
|
291
|
+
first = entries[0]
|
|
292
|
+
item: Dict[str, Any] = dict(first)
|
|
293
|
+
item["count"] = len(entries)
|
|
294
|
+
item["message"] = f"[聚合] {first.get('message', '')} (重复 {len(entries)} 次)"
|
|
295
|
+
if self._include_samples:
|
|
296
|
+
item["sample"] = {"first": entries[0], "last": entries[-1]}
|
|
297
|
+
aggregated.append(item)
|
|
298
|
+
|
|
299
|
+
self._buffer.clear()
|
|
300
|
+
self._last_flush_at = time.time()
|
|
301
|
+
|
|
302
|
+
if self._on_flush is not None:
|
|
303
|
+
try:
|
|
304
|
+
self._on_flush(aggregated)
|
|
305
|
+
except Exception:
|
|
306
|
+
_logger.exception("LogAggregator on_flush 执行失败")
|
|
307
|
+
else:
|
|
308
|
+
try:
|
|
309
|
+
self._output.put_nowait(aggregated)
|
|
310
|
+
except queue.Full:
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
return aggregated
|
|
314
|
+
|
|
315
|
+
def _worker(self) -> None:
|
|
316
|
+
while not self._stop.is_set():
|
|
317
|
+
self._stop.wait(self.flush_interval)
|
|
318
|
+
if self._stop.is_set():
|
|
319
|
+
break
|
|
320
|
+
with self._lock:
|
|
321
|
+
if self._buffer and (time.time() - self._last_flush_at) >= self.flush_interval:
|
|
322
|
+
self._flush_locked()
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
class PerformanceMonitor:
|
|
326
|
+
def __init__(self, sample_interval: float = 5.0):
|
|
327
|
+
self._lock = threading.Lock()
|
|
328
|
+
self._stop = threading.Event()
|
|
329
|
+
self._sample_interval = max(0.5, float(sample_interval))
|
|
330
|
+
|
|
331
|
+
self._start_time = time.time()
|
|
332
|
+
self._processing_times: deque[float] = deque(maxlen=1000)
|
|
333
|
+
self._metrics: Dict[str, Any] = {
|
|
334
|
+
"log_count": 0,
|
|
335
|
+
"error_count": 0,
|
|
336
|
+
"avg_processing_time_ms": 0.0,
|
|
337
|
+
"memory_usage_mb": None,
|
|
338
|
+
"cpu_usage_percent": None,
|
|
339
|
+
"throughput_per_sec": 0.0,
|
|
340
|
+
"updated_at": _now_iso(),
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
344
|
+
self._thread.start()
|
|
345
|
+
|
|
346
|
+
def record_log(self, level: str, processing_time_sec: float) -> None:
|
|
347
|
+
with self._lock:
|
|
348
|
+
self._metrics["log_count"] += 1
|
|
349
|
+
if str(level).upper() == "ERROR":
|
|
350
|
+
self._metrics["error_count"] += 1
|
|
351
|
+
self._processing_times.append(float(processing_time_sec))
|
|
352
|
+
if self._processing_times:
|
|
353
|
+
avg_ms = (sum(self._processing_times) / len(self._processing_times)) * 1000.0
|
|
354
|
+
self._metrics["avg_processing_time_ms"] = avg_ms
|
|
355
|
+
|
|
356
|
+
def get_metrics(self) -> Dict[str, Any]:
|
|
357
|
+
with self._lock:
|
|
358
|
+
return dict(self._metrics)
|
|
359
|
+
|
|
360
|
+
def stop(self) -> None:
|
|
361
|
+
self._stop.set()
|
|
362
|
+
self._thread.join(timeout=2)
|
|
363
|
+
|
|
364
|
+
def _worker(self) -> None:
|
|
365
|
+
process = None
|
|
366
|
+
if psutil is not None:
|
|
367
|
+
try:
|
|
368
|
+
process = psutil.Process()
|
|
369
|
+
process.cpu_percent(interval=None)
|
|
370
|
+
except Exception:
|
|
371
|
+
process = None
|
|
372
|
+
|
|
373
|
+
while not self._stop.is_set():
|
|
374
|
+
self._stop.wait(self._sample_interval)
|
|
375
|
+
if self._stop.is_set():
|
|
376
|
+
break
|
|
377
|
+
with self._lock:
|
|
378
|
+
if process is not None:
|
|
379
|
+
try:
|
|
380
|
+
self._metrics["memory_usage_mb"] = process.memory_info().rss / 1024 / 1024
|
|
381
|
+
self._metrics["cpu_usage_percent"] = process.cpu_percent(interval=None)
|
|
382
|
+
except Exception:
|
|
383
|
+
self._metrics["memory_usage_mb"] = None
|
|
384
|
+
self._metrics["cpu_usage_percent"] = None
|
|
385
|
+
|
|
386
|
+
elapsed = time.time() - self._start_time
|
|
387
|
+
if elapsed > 0:
|
|
388
|
+
self._metrics["throughput_per_sec"] = self._metrics["log_count"] / elapsed
|
|
389
|
+
self._metrics["updated_at"] = _now_iso()
|
|
390
|
+
|
|
391
|
+
|
|
326
392
|
class LogArchiver:
|
|
327
|
-
"""日志归档器"""
|
|
328
393
|
def __init__(self, archive_dir: str = "archives"):
|
|
329
|
-
self.archive_dir = archive_dir
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
394
|
+
self.archive_dir = _ensure_dir(archive_dir)
|
|
395
|
+
|
|
396
|
+
def compress_file(
|
|
397
|
+
self,
|
|
398
|
+
file_path: str,
|
|
399
|
+
compression_type: str = "gzip",
|
|
400
|
+
output_name: Optional[str] = None,
|
|
401
|
+
) -> str:
|
|
402
|
+
src = Path(file_path)
|
|
403
|
+
if not src.exists() or not src.is_file():
|
|
404
|
+
raise FileNotFoundError(str(src))
|
|
405
|
+
|
|
406
|
+
compression_type = str(compression_type).lower()
|
|
407
|
+
if compression_type not in {"gzip", "zip"}:
|
|
408
|
+
raise ValueError("compression_type 仅支持 gzip/zip")
|
|
409
|
+
|
|
410
|
+
base_name = output_name or src.name
|
|
411
|
+
out_path = Path(self.archive_dir) / base_name
|
|
334
412
|
if compression_type == "gzip":
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
413
|
+
if not str(out_path).endswith(".gz"):
|
|
414
|
+
out_path = out_path.with_suffix(out_path.suffix + ".gz")
|
|
415
|
+
import gzip
|
|
416
|
+
|
|
417
|
+
with open(src, "rb") as f_in, gzip.open(out_path, "wb") as f_out:
|
|
418
|
+
shutil.copyfileobj(f_in, f_out)
|
|
419
|
+
return str(out_path)
|
|
420
|
+
|
|
421
|
+
if not str(out_path).endswith(".zip"):
|
|
422
|
+
out_path = out_path.with_suffix(out_path.suffix + ".zip")
|
|
423
|
+
with zipfile.ZipFile(out_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
|
424
|
+
zf.write(str(src), arcname=src.name)
|
|
425
|
+
return str(out_path)
|
|
426
|
+
|
|
427
|
+
def archive_logs(
|
|
428
|
+
self,
|
|
429
|
+
log_dir: str,
|
|
430
|
+
days_old: int = 7,
|
|
431
|
+
compression_type: str = "gzip",
|
|
432
|
+
delete_original: bool = True,
|
|
433
|
+
) -> List[str]:
|
|
434
|
+
log_path = Path(log_dir)
|
|
435
|
+
if not log_path.exists():
|
|
436
|
+
return []
|
|
437
|
+
|
|
438
|
+
archived: List[str] = []
|
|
439
|
+
cutoff = time.time() - (max(0, int(days_old)) * 86400)
|
|
440
|
+
for fp in log_path.glob("*.log"):
|
|
441
|
+
try:
|
|
442
|
+
if fp.stat().st_mtime > cutoff:
|
|
443
|
+
continue
|
|
444
|
+
out = self.compress_file(str(fp), compression_type=compression_type)
|
|
445
|
+
archived.append(out)
|
|
446
|
+
if delete_original:
|
|
447
|
+
fp.unlink(missing_ok=True)
|
|
448
|
+
except Exception:
|
|
449
|
+
_logger.exception("归档失败: %s", fp)
|
|
450
|
+
return archived
|
|
451
|
+
|
|
452
|
+
|
|
368
453
|
class LogDatabase:
|
|
369
|
-
|
|
370
|
-
|
|
454
|
+
_allowed_columns = {
|
|
455
|
+
"id",
|
|
456
|
+
"timestamp",
|
|
457
|
+
"level",
|
|
458
|
+
"message",
|
|
459
|
+
"file",
|
|
460
|
+
"line",
|
|
461
|
+
"function",
|
|
462
|
+
"process_id",
|
|
463
|
+
"thread_id",
|
|
464
|
+
"extra_data",
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
def __init__(
|
|
468
|
+
self,
|
|
469
|
+
db_path: str = "logs.db",
|
|
470
|
+
enable_wal: bool = True,
|
|
471
|
+
busy_timeout_ms: int = 5000,
|
|
472
|
+
):
|
|
371
473
|
self.db_path = db_path
|
|
474
|
+
self._lock = threading.Lock()
|
|
475
|
+
self._conn = sqlite3.connect(self.db_path, check_same_thread=False)
|
|
476
|
+
self._conn.row_factory = sqlite3.Row
|
|
477
|
+
if enable_wal:
|
|
478
|
+
try:
|
|
479
|
+
self._conn.execute("PRAGMA journal_mode=WAL;")
|
|
480
|
+
except Exception:
|
|
481
|
+
pass
|
|
482
|
+
try:
|
|
483
|
+
self._conn.execute(f"PRAGMA busy_timeout={int(busy_timeout_ms)};")
|
|
484
|
+
except Exception:
|
|
485
|
+
pass
|
|
372
486
|
self._init_database()
|
|
373
|
-
|
|
487
|
+
|
|
488
|
+
def close(self) -> None:
|
|
489
|
+
with self._lock:
|
|
490
|
+
try:
|
|
491
|
+
self._conn.close()
|
|
492
|
+
except Exception:
|
|
493
|
+
pass
|
|
494
|
+
|
|
374
495
|
def _init_database(self) -> None:
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
496
|
+
with self._lock:
|
|
497
|
+
self._conn.execute(
|
|
498
|
+
"""
|
|
378
499
|
CREATE TABLE IF NOT EXISTS logs (
|
|
379
500
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
380
501
|
timestamp TEXT NOT NULL,
|
|
@@ -387,281 +508,324 @@ class LogDatabase:
|
|
|
387
508
|
thread_id INTEGER,
|
|
388
509
|
extra_data TEXT
|
|
389
510
|
)
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
511
|
+
"""
|
|
512
|
+
)
|
|
513
|
+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_logs_timestamp ON logs(timestamp)")
|
|
514
|
+
self._conn.execute("CREATE INDEX IF NOT EXISTS idx_logs_level ON logs(level)")
|
|
515
|
+
self._conn.commit()
|
|
516
|
+
|
|
517
|
+
def insert_log(self, log_entry: Mapping[str, Any]) -> None:
|
|
518
|
+
self.insert_many([log_entry])
|
|
519
|
+
|
|
520
|
+
def insert_many(self, log_entries: Sequence[Mapping[str, Any]]) -> None:
|
|
521
|
+
rows = []
|
|
522
|
+
for e in log_entries:
|
|
523
|
+
rows.append(
|
|
524
|
+
(
|
|
525
|
+
e.get("timestamp") or _now_iso(),
|
|
526
|
+
e.get("level") or "INFO",
|
|
527
|
+
e.get("message") or "",
|
|
528
|
+
e.get("file"),
|
|
529
|
+
e.get("line"),
|
|
530
|
+
e.get("function"),
|
|
531
|
+
e.get("process_id"),
|
|
532
|
+
e.get("thread_id"),
|
|
533
|
+
json.dumps(e.get("extra_data") or {}, ensure_ascii=False),
|
|
534
|
+
)
|
|
535
|
+
)
|
|
536
|
+
with self._lock:
|
|
537
|
+
self._conn.executemany(
|
|
538
|
+
"""
|
|
404
539
|
INSERT INTO logs (timestamp, level, message, file, line, function, process_id, thread_id, extra_data)
|
|
405
540
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
"""查询日志"""
|
|
420
|
-
query = "SELECT * FROM logs"
|
|
421
|
-
params = []
|
|
422
|
-
|
|
541
|
+
""",
|
|
542
|
+
rows,
|
|
543
|
+
)
|
|
544
|
+
self._conn.commit()
|
|
545
|
+
|
|
546
|
+
def query_logs(
|
|
547
|
+
self,
|
|
548
|
+
conditions: Optional[Mapping[str, Any]] = None,
|
|
549
|
+
limit: int = 1000,
|
|
550
|
+
order_desc: bool = True,
|
|
551
|
+
) -> List[Dict[str, Any]]:
|
|
552
|
+
where_parts: List[str] = []
|
|
553
|
+
params: List[Any] = []
|
|
423
554
|
if conditions:
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
555
|
+
for k, v in conditions.items():
|
|
556
|
+
if k not in self._allowed_columns:
|
|
557
|
+
raise ValueError(f"不支持的查询字段: {k}")
|
|
558
|
+
where_parts.append(f"{k} = ?")
|
|
559
|
+
params.append(v)
|
|
560
|
+
|
|
561
|
+
sql = "SELECT * FROM logs"
|
|
562
|
+
if where_parts:
|
|
563
|
+
sql += " WHERE " + " AND ".join(where_parts)
|
|
564
|
+
sql += " ORDER BY timestamp " + ("DESC" if order_desc else "ASC")
|
|
565
|
+
sql += " LIMIT ?"
|
|
566
|
+
params.append(int(limit))
|
|
567
|
+
|
|
568
|
+
with self._lock:
|
|
569
|
+
cur = self._conn.execute(sql, params)
|
|
570
|
+
result = []
|
|
571
|
+
for row in cur.fetchall():
|
|
572
|
+
d = dict(row)
|
|
573
|
+
try:
|
|
574
|
+
d["extra_data"] = json.loads(d.get("extra_data") or "{}")
|
|
575
|
+
except Exception:
|
|
576
|
+
pass
|
|
577
|
+
result.append(d)
|
|
578
|
+
return result
|
|
579
|
+
|
|
580
|
+
def purge_older_than(self, days: int) -> int:
|
|
581
|
+
days = int(days)
|
|
582
|
+
if days <= 0:
|
|
583
|
+
return 0
|
|
584
|
+
cutoff = datetime.fromtimestamp(time.time() - days * 86400).isoformat(timespec="seconds")
|
|
585
|
+
with self._lock:
|
|
586
|
+
cur = self._conn.execute("DELETE FROM logs WHERE timestamp < ?", (cutoff,))
|
|
587
|
+
self._conn.commit()
|
|
588
|
+
return int(cur.rowcount or 0)
|
|
589
|
+
|
|
590
|
+
|
|
439
591
|
class LogStreamProcessor:
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
self.
|
|
447
|
-
self.
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
""
|
|
592
|
+
def __init__(
|
|
593
|
+
self,
|
|
594
|
+
processors: Optional[List[Callable[[Dict[str, Any]], Dict[str, Any]]]] = None,
|
|
595
|
+
max_queue_size: int = 10000,
|
|
596
|
+
error_handler: Optional[Callable[[Exception, Dict[str, Any]], None]] = None,
|
|
597
|
+
):
|
|
598
|
+
self.processors = list(processors or [])
|
|
599
|
+
self._error_handler = error_handler
|
|
600
|
+
|
|
601
|
+
self._input: "queue.Queue[Optional[Dict[str, Any]]]" = queue.Queue(maxsize=max(1, int(max_queue_size)))
|
|
602
|
+
self._output: "queue.Queue[Dict[str, Any]]" = queue.Queue(maxsize=max(1, int(max_queue_size)))
|
|
603
|
+
self._stop = threading.Event()
|
|
604
|
+
self._thread = threading.Thread(target=self._worker, daemon=True)
|
|
605
|
+
self._thread.start()
|
|
606
|
+
|
|
607
|
+
def add_processor(self, processor: Callable[[Dict[str, Any]], Dict[str, Any]]) -> None:
|
|
451
608
|
self.processors.append(processor)
|
|
452
|
-
|
|
453
|
-
def process_log(self, log_entry: Dict[str, Any]) ->
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
609
|
+
|
|
610
|
+
def process_log(self, log_entry: Dict[str, Any], block: bool = True, timeout: Optional[float] = None) -> bool:
|
|
611
|
+
try:
|
|
612
|
+
self._input.put(log_entry, block=block, timeout=timeout)
|
|
613
|
+
return True
|
|
614
|
+
except queue.Full:
|
|
615
|
+
return False
|
|
616
|
+
|
|
617
|
+
def get_processed_log(self, timeout: float = 0.0) -> Optional[Dict[str, Any]]:
|
|
618
|
+
try:
|
|
619
|
+
return self._output.get(timeout=timeout)
|
|
620
|
+
except queue.Empty:
|
|
621
|
+
return None
|
|
622
|
+
|
|
623
|
+
def stop(self, timeout: float = 2.0) -> None:
|
|
624
|
+
self._stop.set()
|
|
625
|
+
try:
|
|
626
|
+
self._input.put_nowait(None)
|
|
627
|
+
except Exception:
|
|
628
|
+
pass
|
|
629
|
+
self._thread.join(timeout=timeout)
|
|
630
|
+
|
|
631
|
+
def _handle_error(self, exc: Exception, entry: Dict[str, Any]) -> None:
|
|
632
|
+
if self._error_handler is not None:
|
|
460
633
|
try:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
634
|
+
self._error_handler(exc, entry)
|
|
635
|
+
except Exception:
|
|
636
|
+
_logger.exception("LogStreamProcessor error_handler 执行失败")
|
|
637
|
+
return
|
|
638
|
+
_logger.exception("日志处理失败")
|
|
639
|
+
|
|
640
|
+
def _worker(self) -> None:
|
|
641
|
+
while not self._stop.is_set():
|
|
642
|
+
try:
|
|
643
|
+
item = self._input.get(timeout=0.5)
|
|
468
644
|
except queue.Empty:
|
|
469
645
|
continue
|
|
646
|
+
if item is None:
|
|
647
|
+
break
|
|
648
|
+
processed = item
|
|
649
|
+
try:
|
|
650
|
+
for p in self.processors:
|
|
651
|
+
processed = p(processed)
|
|
652
|
+
try:
|
|
653
|
+
self._output.put_nowait(processed)
|
|
654
|
+
except queue.Full:
|
|
655
|
+
pass
|
|
470
656
|
except Exception as e:
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
def get_processed_log(self) -> Optional[Dict[str, Any]]:
|
|
474
|
-
"""获取处理后的日志"""
|
|
475
|
-
try:
|
|
476
|
-
return self.output_queue.get_nowait()
|
|
477
|
-
except queue.Empty:
|
|
478
|
-
return None
|
|
657
|
+
self._handle_error(e, item)
|
|
658
|
+
|
|
479
659
|
|
|
480
|
-
# 新增:智能日志分析
|
|
481
660
|
class LogAnalyzer:
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
r
|
|
487
|
-
r
|
|
488
|
-
r
|
|
489
|
-
r'MySQL.*error'
|
|
661
|
+
def __init__(self, patterns: Optional[Mapping[str, Sequence[str]]] = None):
|
|
662
|
+
self.patterns: Dict[str, List[str]] = {
|
|
663
|
+
"error": [
|
|
664
|
+
r"Exception|Error|Failed|Timeout|Connection refused",
|
|
665
|
+
r"HTTP \d{3}",
|
|
666
|
+
r"ORA-\d{5}",
|
|
667
|
+
r"MySQL.*error",
|
|
490
668
|
],
|
|
491
|
-
|
|
492
|
-
r
|
|
493
|
-
r
|
|
494
|
-
r
|
|
669
|
+
"warning": [
|
|
670
|
+
r"Warning|Deprecated|Deprecation",
|
|
671
|
+
r"Slow query|Performance issue",
|
|
672
|
+
r"Resource.*low|Memory.*high",
|
|
673
|
+
],
|
|
674
|
+
"security": [
|
|
675
|
+
r"Unauthorized|Forbidden|Authentication failed",
|
|
676
|
+
r"SQL injection|XSS|CSRF",
|
|
677
|
+
r"Failed login|Invalid credentials",
|
|
495
678
|
],
|
|
496
|
-
'security_patterns': [
|
|
497
|
-
r'Unauthorized|Forbidden|Authentication failed',
|
|
498
|
-
r'SQL injection|XSS|CSRF',
|
|
499
|
-
r'Failed login|Invalid credentials'
|
|
500
|
-
]
|
|
501
|
-
}
|
|
502
|
-
self.compiled_patterns = {}
|
|
503
|
-
for category, patterns in self.patterns.items():
|
|
504
|
-
self.compiled_patterns[category] = [re.compile(pattern, re.IGNORECASE) for pattern in patterns]
|
|
505
|
-
|
|
506
|
-
def analyze_log(self, log_entry: Dict[str, Any]) -> Dict[str, Any]:
|
|
507
|
-
"""分析日志"""
|
|
508
|
-
message = log_entry.get('message', '')
|
|
509
|
-
level = log_entry.get('level', 'INFO')
|
|
510
|
-
|
|
511
|
-
analysis = {
|
|
512
|
-
'severity': 'normal',
|
|
513
|
-
'categories': [],
|
|
514
|
-
'suggestions': [],
|
|
515
|
-
'patterns_found': []
|
|
516
679
|
}
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
680
|
+
if patterns:
|
|
681
|
+
for k, v in patterns.items():
|
|
682
|
+
self.patterns[str(k)] = list(v)
|
|
683
|
+
|
|
684
|
+
self._compiled: Dict[str, List[re.Pattern[str]]] = {}
|
|
685
|
+
for category, pats in self.patterns.items():
|
|
686
|
+
self._compiled[category] = [re.compile(p, re.IGNORECASE) for p in pats]
|
|
687
|
+
|
|
688
|
+
def analyze_log(self, log_entry: Mapping[str, Any]) -> Dict[str, Any]:
|
|
689
|
+
message = str(log_entry.get("message") or "")
|
|
690
|
+
|
|
691
|
+
categories: List[str] = []
|
|
692
|
+
patterns_found: List[str] = []
|
|
693
|
+
severity = "normal"
|
|
694
|
+
|
|
695
|
+
for pattern in self._compiled.get("error", []):
|
|
520
696
|
if pattern.search(message):
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
# 检查警告模式
|
|
526
|
-
for pattern in self.compiled_patterns['warning_patterns']:
|
|
697
|
+
categories.append("error")
|
|
698
|
+
patterns_found.append(pattern.pattern)
|
|
699
|
+
severity = "high"
|
|
700
|
+
for pattern in self._compiled.get("warning", []):
|
|
527
701
|
if pattern.search(message):
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
# 检查安全模式
|
|
534
|
-
for pattern in self.compiled_patterns['security_patterns']:
|
|
702
|
+
categories.append("warning")
|
|
703
|
+
patterns_found.append(pattern.pattern)
|
|
704
|
+
if severity == "normal":
|
|
705
|
+
severity = "medium"
|
|
706
|
+
for pattern in self._compiled.get("security", []):
|
|
535
707
|
if pattern.search(message):
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
if
|
|
542
|
-
|
|
543
|
-
if
|
|
544
|
-
|
|
545
|
-
if
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
return
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
def __init__(self):
|
|
554
|
-
self.health_metrics = {
|
|
555
|
-
'total_logs': 0,
|
|
556
|
-
'error_rate': 0.0,
|
|
557
|
-
'avg_response_time': 0.0,
|
|
558
|
-
'memory_usage': 0.0,
|
|
559
|
-
'disk_usage': 0.0,
|
|
560
|
-
'last_check': None
|
|
708
|
+
categories.append("security")
|
|
709
|
+
patterns_found.append(pattern.pattern)
|
|
710
|
+
severity = "critical"
|
|
711
|
+
|
|
712
|
+
suggestions: List[str] = []
|
|
713
|
+
if "error" in categories:
|
|
714
|
+
suggestions.append("检查相关服务和依赖")
|
|
715
|
+
if "security" in categories:
|
|
716
|
+
suggestions.append("立即检查安全配置")
|
|
717
|
+
if "warning" in categories:
|
|
718
|
+
suggestions.append("监控系统性能")
|
|
719
|
+
|
|
720
|
+
return {
|
|
721
|
+
"severity": severity,
|
|
722
|
+
"categories": list(dict.fromkeys(categories)),
|
|
723
|
+
"suggestions": suggestions,
|
|
724
|
+
"patterns_found": patterns_found,
|
|
561
725
|
}
|
|
562
|
-
|
|
726
|
+
|
|
727
|
+
|
|
728
|
+
class LogHealthChecker:
|
|
563
729
|
def check_health(self, log_dir: str) -> Dict[str, Any]:
|
|
564
|
-
"""检查日志系统健康状态"""
|
|
565
730
|
try:
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
731
|
+
total, used, _ = shutil.disk_usage(log_dir)
|
|
732
|
+
disk_usage_percent = (used / total) * 100 if total else 0.0
|
|
733
|
+
|
|
734
|
+
memory_usage_mb = None
|
|
735
|
+
if psutil is not None:
|
|
736
|
+
try:
|
|
737
|
+
process = psutil.Process()
|
|
738
|
+
memory_usage_mb = process.memory_info().rss / 1024 / 1024
|
|
739
|
+
except Exception:
|
|
740
|
+
memory_usage_mb = None
|
|
741
|
+
|
|
575
742
|
log_files = list(Path(log_dir).glob("*.log"))
|
|
576
|
-
total_size = sum(f.stat().st_size for f in log_files)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
'disk_usage_percent': disk_usage_percent,
|
|
581
|
-
'memory_usage_mb': memory_usage,
|
|
582
|
-
'log_files_count': len(log_files),
|
|
583
|
-
'total_log_size_mb': total_size / 1024 / 1024,
|
|
584
|
-
'last_check': datetime.now().isoformat()
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
# 判断健康状态
|
|
743
|
+
total_size = sum(f.stat().st_size for f in log_files) if log_files else 0
|
|
744
|
+
|
|
745
|
+
status = "healthy"
|
|
746
|
+
warnings: List[str] = []
|
|
588
747
|
if disk_usage_percent > 90:
|
|
589
|
-
|
|
590
|
-
|
|
748
|
+
status = "critical"
|
|
749
|
+
warnings.append("磁盘使用率过高")
|
|
591
750
|
elif disk_usage_percent > 80:
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
if
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
'last_check': datetime.now().isoformat()
|
|
751
|
+
status = "warning"
|
|
752
|
+
warnings.append("磁盘使用率较高")
|
|
753
|
+
|
|
754
|
+
if memory_usage_mb is not None and memory_usage_mb > 1024:
|
|
755
|
+
if status == "healthy":
|
|
756
|
+
status = "warning"
|
|
757
|
+
warnings.append("内存使用量较高")
|
|
758
|
+
|
|
759
|
+
result: Dict[str, Any] = {
|
|
760
|
+
"status": status,
|
|
761
|
+
"disk_usage_percent": float(disk_usage_percent),
|
|
762
|
+
"memory_usage_mb": memory_usage_mb,
|
|
763
|
+
"log_files_count": len(log_files),
|
|
764
|
+
"total_log_size_mb": total_size / 1024 / 1024,
|
|
765
|
+
"checked_at": _now_iso(),
|
|
608
766
|
}
|
|
767
|
+
if warnings:
|
|
768
|
+
result["warnings"] = warnings
|
|
769
|
+
return result
|
|
770
|
+
except Exception as e:
|
|
771
|
+
return {"status": "error", "error": str(e), "checked_at": _now_iso()}
|
|
772
|
+
|
|
609
773
|
|
|
610
|
-
# 新增:日志备份和恢复
|
|
611
774
|
class LogBackupManager:
|
|
612
|
-
"""日志备份管理器"""
|
|
613
775
|
def __init__(self, backup_dir: str = "backups"):
|
|
614
|
-
self.backup_dir = backup_dir
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
""
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
backup_path = os.path.join(self.backup_dir, f"{backup_name}.tar.gz")
|
|
623
|
-
|
|
624
|
-
with tarfile.open(backup_path, 'w:gz') as tar:
|
|
776
|
+
self.backup_dir = _ensure_dir(backup_dir)
|
|
777
|
+
|
|
778
|
+
def create_backup(self, log_dir: str, backup_name: Optional[str] = None) -> str:
|
|
779
|
+
backup_name = backup_name or f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
|
780
|
+
backup_path = str(Path(self.backup_dir) / f"{backup_name}.tar.gz")
|
|
781
|
+
|
|
782
|
+
with tarfile.open(backup_path, "w:gz") as tar:
|
|
625
783
|
for log_file in Path(log_dir).glob("*.log"):
|
|
626
|
-
tar.add(log_file, arcname=log_file.name)
|
|
627
|
-
|
|
784
|
+
tar.add(str(log_file), arcname=log_file.name)
|
|
628
785
|
return backup_path
|
|
629
|
-
|
|
786
|
+
|
|
630
787
|
def restore_backup(self, backup_path: str, restore_dir: str) -> bool:
|
|
631
|
-
"""恢复日志备份"""
|
|
632
788
|
try:
|
|
633
|
-
|
|
634
|
-
|
|
789
|
+
base = Path(restore_dir)
|
|
790
|
+
_ensure_dir(base)
|
|
791
|
+
with tarfile.open(backup_path, "r:gz") as tar:
|
|
792
|
+
for member in tar.getmembers():
|
|
793
|
+
if not member.name or member.name.startswith("/") or ".." in Path(member.name).parts:
|
|
794
|
+
raise RuntimeError(f"不安全的备份成员路径: {member.name}")
|
|
795
|
+
target = base / member.name
|
|
796
|
+
if not _is_within_directory(base, target):
|
|
797
|
+
raise RuntimeError(f"不安全的备份成员路径: {member.name}")
|
|
798
|
+
tar.extractall(str(base))
|
|
635
799
|
return True
|
|
636
|
-
except Exception
|
|
637
|
-
|
|
800
|
+
except Exception:
|
|
801
|
+
_logger.exception("恢复备份失败")
|
|
638
802
|
return False
|
|
639
|
-
|
|
803
|
+
|
|
640
804
|
def list_backups(self) -> List[Dict[str, Any]]:
|
|
641
|
-
|
|
642
|
-
backups = []
|
|
805
|
+
backups: List[Dict[str, Any]] = []
|
|
643
806
|
for backup_file in Path(self.backup_dir).glob("*.tar.gz"):
|
|
644
807
|
stat = backup_file.stat()
|
|
645
|
-
backups.append(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
808
|
+
backups.append(
|
|
809
|
+
{
|
|
810
|
+
"name": backup_file.name,
|
|
811
|
+
"path": str(backup_file),
|
|
812
|
+
"size_mb": stat.st_size / 1024 / 1024,
|
|
813
|
+
"created": datetime.fromtimestamp(stat.st_mtime).isoformat(timespec="seconds"),
|
|
814
|
+
}
|
|
815
|
+
)
|
|
816
|
+
return sorted(backups, key=lambda x: x["created"], reverse=True)
|
|
817
|
+
|
|
818
|
+
|
|
653
819
|
__all__ = [
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
'LogBackupManager'
|
|
667
|
-
]
|
|
820
|
+
"LogFilter",
|
|
821
|
+
"LogSecurity",
|
|
822
|
+
"DistributedLogger",
|
|
823
|
+
"LogAggregator",
|
|
824
|
+
"PerformanceMonitor",
|
|
825
|
+
"LogArchiver",
|
|
826
|
+
"LogDatabase",
|
|
827
|
+
"LogStreamProcessor",
|
|
828
|
+
"LogAnalyzer",
|
|
829
|
+
"LogHealthChecker",
|
|
830
|
+
"LogBackupManager",
|
|
831
|
+
]
|