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.
@@ -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 asyncio
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 hashlib
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 gzip
16
+ import sqlite3
43
17
  import tarfile
18
+ import threading
19
+ import time
44
20
  import zipfile
45
- import base64
46
- import hmac
47
- import secrets
48
- import ssl
49
- import certifi
50
- import urllib3
51
- from urllib3.util.retry import Retry
52
- from urllib3.util import Timeout
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
- class LogAggregator:
64
- """日志聚合器"""
65
- def __init__(self, window_size: int = 100, flush_interval: float = 5.0):
66
- self.window_size = window_size
67
- self.flush_interval = flush_interval
68
- self.buffer = deque(maxlen=window_size)
69
- self.last_flush = time.time()
70
- self.lock = threading.Lock()
71
- self._running = True
72
- self._flush_thread = threading.Thread(target=self._flush_worker, daemon=True)
73
- self._flush_thread.start()
74
-
75
- def add_log(self, log_entry: Dict[str, Any]) -> None:
76
- """添加日志到缓冲区"""
77
- with self.lock:
78
- self.buffer.append(log_entry)
79
- if len(self.buffer) >= self.window_size:
80
- self._flush_buffer()
81
-
82
- def _flush_buffer(self) -> None:
83
- """刷新缓冲区"""
84
- if not self.buffer:
85
- return
86
-
87
- # 聚合日志
88
- aggregated = self._aggregate_logs()
89
- # 这里可以发送到外部系统或存储
90
- print(f"聚合日志: {len(self.buffer)} 条 -> {len(aggregated)} 条")
91
- self.buffer.clear()
92
- self.last_flush = time.time()
93
-
94
- def _aggregate_logs(self) -> List[Dict[str, Any]]:
95
- """聚合日志"""
96
- if not self.buffer:
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
- groups = defaultdict(list)
101
- for log in self.buffer:
102
- key = f"{log.get('level', 'INFO')}:{log.get('message', '')[:50]}"
103
- groups[key].append(log)
104
-
105
- aggregated = []
106
- for key, logs in groups.items():
107
- if len(logs) == 1:
108
- aggregated.append(logs[0])
109
- else:
110
- # 创建聚合日志
111
- first_log = logs[0]
112
- aggregated_log = {
113
- 'level': first_log.get('level', 'INFO'),
114
- 'message': f"[聚合] {first_log.get('message', '')} (重复 {len(logs)} 次)",
115
- 'timestamp': first_log.get('timestamp'),
116
- 'count': len(logs),
117
- 'original_logs': logs
118
- }
119
- aggregated.append(aggregated_log)
120
-
121
- return aggregated
122
-
123
- def _flush_worker(self) -> None:
124
- """后台刷新工作线程"""
125
- while self._running:
126
- time.sleep(self.flush_interval)
127
- with self.lock:
128
- if self.buffer and time.time() - self.last_flush > self.flush_interval:
129
- self._flush_buffer()
130
-
131
- def stop(self) -> None:
132
- """停止聚合器"""
133
- self._running = False
134
- self._flush_buffer()
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
- def __init__(self, node_id: str, cluster_nodes: List[str] = None):
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.cluster_nodes = cluster_nodes or []
194
- self.sequence_number = 0
195
- self.lock = threading.Lock()
196
- self._sequence_file = f"sequence_{node_id}.dat"
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, 'r') as f:
204
- self.sequence_number = int(f.read().strip())
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.sequence_number = 0
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(self._sequence_file, 'w') as f:
212
- f.write(str(self.sequence_number))
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
- pass
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
- """获取唯一日志ID"""
218
- with self.lock:
219
- self.sequence_number += 1
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
- timestamp = int(time.time() * 1000)
222
- return f"{self.node_id}_{timestamp}_{self.sequence_number}"
223
-
224
- # 新增:内存优化和垃圾回收
225
- class MemoryOptimizer:
226
- """内存优化器"""
227
- def __init__(self, max_memory_mb: int = 512):
228
- self.max_memory_mb = max_memory_mb
229
- self.last_gc_time = time.time()
230
- self.gc_interval = 60 # 60秒执行一次GC
231
- self._gc_thread = threading.Thread(target=self._gc_worker, daemon=True)
232
- self._gc_thread.start()
233
-
234
- def check_memory(self) -> bool:
235
- """检查内存使用情况"""
236
- process = psutil.Process()
237
- memory_mb = process.memory_info().rss / 1024 / 1024
238
- return memory_mb > self.max_memory_mb
239
-
240
- def optimize_memory(self) -> None:
241
- """优化内存使用"""
242
- if self.check_memory():
243
- # 强制垃圾回收
244
- collected = gc.collect()
245
- print(f"内存优化: 回收了 {collected} 个对象")
246
-
247
- # 清理缓存
248
- if hasattr(self, '_clear_caches'):
249
- self._clear_caches()
250
-
251
- def _gc_worker(self) -> None:
252
- """垃圾回收工作线程"""
253
- while True:
254
- time.sleep(self.gc_interval)
255
- self.optimize_memory()
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
- from cryptography.fernet import Fernet
292
- self.encryption_key = encryption_key or Fernet.generate_key()
293
- self.cipher = Fernet(self.encryption_key)
294
- except ImportError:
295
- print("警告: cryptography 未安装,加密功能将不可用")
296
- self.cipher = None
297
-
298
- self.sensitive_patterns = [
299
- r'(password["\']?\s*[:=]\s*["\'][^"\']*["\'])',
300
- r'(api_key["\']?\s*[:=]\s*["\'][^"\']*["\'])',
301
- r'(token["\']?\s*[:=]\s*["\'][^"\']*["\'])',
302
- r'(secret["\']?\s*[:=]\s*["\'][^"\']*["\'])'
303
- ]
304
- self.compiled_patterns = [re.compile(pattern, re.IGNORECASE) for pattern in self.sensitive_patterns]
305
-
306
- def sanitize_message(self, message: str) -> str:
307
- """清理敏感信息"""
308
- sanitized = message
309
- for pattern in self.compiled_patterns:
310
- sanitized = pattern.sub(r'\1=***', sanitized)
311
- return sanitized
312
-
313
- def encrypt_log(self, log_data: bytes) -> bytes:
314
- """加密日志数据"""
315
- if self.cipher is None:
316
- return log_data
317
- return self.cipher.encrypt(log_data)
318
-
319
- def decrypt_log(self, encrypted_data: bytes) -> bytes:
320
- """解密日志数据"""
321
- if self.cipher is None:
322
- return encrypted_data
323
- return self.cipher.decrypt(encrypted_data)
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
- os.makedirs(archive_dir, exist_ok=True)
331
-
332
- def compress_file(self, file_path: str, compression_type: str = "gzip") -> str:
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
- archive_path = f"{file_path}.gz"
336
- with open(file_path, 'rb') as f_in:
337
- with gzip.open(archive_path, 'wb') as f_out:
338
- shutil.copyfileobj(f_in, f_out)
339
- elif compression_type == "zip":
340
- archive_path = f"{file_path}.zip"
341
- with zipfile.ZipFile(archive_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
342
- zipf.write(file_path, os.path.basename(file_path))
343
- elif compression_type == "tar":
344
- archive_path = f"{file_path}.tar.gz"
345
- with tarfile.open(archive_path, 'w:gz') as tar:
346
- tar.add(file_path, arcname=os.path.basename(file_path))
347
-
348
- return archive_path
349
-
350
- def archive_logs(self, log_dir: str, days_old: int = 7) -> List[str]:
351
- """归档旧日志"""
352
- archived_files = []
353
- current_time = datetime.now()
354
-
355
- for file_path in Path(log_dir).glob("*.log"):
356
- file_time = datetime.fromtimestamp(file_path.stat().st_mtime)
357
- if (current_time - file_time).days >= days_old:
358
- try:
359
- archive_path = self.compress_file(str(file_path))
360
- os.remove(file_path)
361
- archived_files.append(archive_path)
362
- except Exception as e:
363
- print(f"归档文件失败 {file_path}: {e}")
364
-
365
- return archived_files
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
- def __init__(self, db_path: str = "logs.db"):
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
- with sqlite3.connect(self.db_path) as conn:
377
- conn.execute("""
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
- conn.execute("""
393
- CREATE INDEX IF NOT EXISTS idx_timestamp ON logs(timestamp)
394
- """)
395
-
396
- conn.execute("""
397
- CREATE INDEX IF NOT EXISTS idx_level ON logs(level)
398
- """)
399
-
400
- def insert_log(self, log_entry: Dict[str, Any]) -> None:
401
- """插入日志记录"""
402
- with sqlite3.connect(self.db_path) as conn:
403
- conn.execute("""
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
- log_entry.get('timestamp'),
408
- log_entry.get('level'),
409
- log_entry.get('message'),
410
- log_entry.get('file'),
411
- log_entry.get('line'),
412
- log_entry.get('function'),
413
- log_entry.get('process_id'),
414
- log_entry.get('thread_id'),
415
- json.dumps(log_entry.get('extra_data', {}))
416
- ))
417
-
418
- def query_logs(self, conditions: Dict[str, Any] = None, limit: int = 1000) -> List[Dict[str, Any]]:
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
- where_clauses = []
425
- for key, value in conditions.items():
426
- where_clauses.append(f"{key} = ?")
427
- params.append(value)
428
- query += " WHERE " + " AND ".join(where_clauses)
429
-
430
- query += " ORDER BY timestamp DESC LIMIT ?"
431
- params.append(limit)
432
-
433
- with sqlite3.connect(self.db_path) as conn:
434
- cursor = conn.execute(query, params)
435
- columns = [description[0] for description in cursor.description]
436
- return [dict(zip(columns, row)) for row in cursor.fetchall()]
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
- def __init__(self, processors: List[Callable] = None):
442
- self.processors = processors or []
443
- self.input_queue = queue.Queue()
444
- self.output_queue = queue.Queue()
445
- self._running = True
446
- self._processor_thread = threading.Thread(target=self._process_worker, daemon=True)
447
- self._processor_thread.start()
448
-
449
- def add_processor(self, processor: Callable) -> None:
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]) -> None:
454
- """处理日志"""
455
- self.input_queue.put(log_entry)
456
-
457
- def _process_worker(self) -> None:
458
- """处理工作线程"""
459
- while self._running:
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
- log_entry = self.input_queue.get(timeout=1)
462
- processed_entry = log_entry
463
-
464
- for processor in self.processors:
465
- processed_entry = processor(processed_entry)
466
-
467
- self.output_queue.put(processed_entry)
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
- print(f"日志处理错误: {e}")
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
- def __init__(self):
484
- self.patterns = {
485
- 'error_patterns': [
486
- r'Exception|Error|Failed|Timeout|Connection refused',
487
- r'HTTP \d{3}',
488
- r'ORA-\d{5}',
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
- 'warning_patterns': [
492
- r'Warning|Deprecated|Deprecation',
493
- r'Slow query|Performance issue',
494
- r'Resource.*low|Memory.*high'
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
- for pattern in self.compiled_patterns['error_patterns']:
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
- analysis['severity'] = 'high'
522
- analysis['categories'].append('error')
523
- analysis['patterns_found'].append(pattern.pattern)
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
- if analysis['severity'] == 'normal':
529
- analysis['severity'] = 'medium'
530
- analysis['categories'].append('warning')
531
- analysis['patterns_found'].append(pattern.pattern)
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
- analysis['severity'] = 'critical'
537
- analysis['categories'].append('security')
538
- analysis['patterns_found'].append(pattern.pattern)
539
-
540
- # 生成建议
541
- if 'error' in analysis['categories']:
542
- analysis['suggestions'].append('检查相关服务和依赖')
543
- if 'security' in analysis['categories']:
544
- analysis['suggestions'].append('立即检查安全配置')
545
- if 'warning' in analysis['categories']:
546
- analysis['suggestions'].append('监控系统性能')
547
-
548
- return analysis
549
-
550
- # 新增:日志健康检查
551
- class LogHealthChecker:
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
- total, used, free = shutil.disk_usage(log_dir)
568
- disk_usage_percent = (used / total) * 100
569
-
570
- # 检查内存使用情况
571
- process = psutil.Process()
572
- memory_usage = process.memory_info().rss / 1024 / 1024 # MB
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
- health_status = {
579
- 'status': 'healthy',
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
- health_status['status'] = 'critical'
590
- health_status['warnings'] = ['磁盘使用率过高']
748
+ status = "critical"
749
+ warnings.append("磁盘使用率过高")
591
750
  elif disk_usage_percent > 80:
592
- health_status['status'] = 'warning'
593
- health_status['warnings'] = ['磁盘使用率较高']
594
-
595
- if memory_usage > 1024: # 超过1GB
596
- health_status['status'] = 'warning'
597
- if 'warnings' not in health_status:
598
- health_status['warnings'] = []
599
- health_status['warnings'].append('内存使用量较高')
600
-
601
- return health_status
602
-
603
- except Exception as e:
604
- return {
605
- 'status': 'error',
606
- 'error': str(e),
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
- os.makedirs(backup_dir, exist_ok=True)
616
-
617
- def create_backup(self, log_dir: str, backup_name: str = None) -> str:
618
- """创建日志备份"""
619
- if backup_name is None:
620
- backup_name = f"backup_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
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
- with tarfile.open(backup_path, 'r:gz') as tar:
634
- tar.extractall(restore_dir)
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 as e:
637
- print(f"恢复备份失败: {e}")
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
- 'name': backup_file.name,
647
- 'size_mb': stat.st_size / 1024 / 1024,
648
- 'created': datetime.fromtimestamp(stat.st_mtime).isoformat()
649
- })
650
- return sorted(backups, key=lambda x: x['created'], reverse=True)
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
- 'LogFilter',
655
- 'LogAggregator',
656
- 'PerformanceMonitor',
657
- 'DistributedLogger',
658
- 'MemoryOptimizer',
659
- 'LogRouter',
660
- 'LogSecurity',
661
- 'LogArchiver',
662
- 'LogDatabase',
663
- 'LogStreamProcessor',
664
- 'LogAnalyzer',
665
- 'LogHealthChecker',
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
+ ]