"""Daily UTF-8 business logs; bounded UI history, no raw requests or message bodies.""" import json import logging import re from collections import deque from contextlib import suppress from datetime import datetime from pathlib import Path UI_LIMIT = 2000 CREDENTIAL = re.compile( r"(?i)(cookie|authorization|sessionid|mstoken|a_bogus|password|token|secret|verifyfp|signature|x-bogus|x-tt-params)" ) def clean(value): text = str(value).replace("\r", r"\r").replace("\n", r"\n") text = re.sub(r"[\x00-\x1f\x7f\u2028\u2029\u202a-\u202e]", " ", text) text = re.sub( r"(?i)([?&](?:sessionid|mstoken|a_bogus|token|verifyfp|signature|x-bogus|x-tt-params)=)[^&\s]+", r"\1[凭据已隐藏]", text, ) text = re.sub( r"(?i)\b(cookie|authorization)[\"']?\s*[:=].*", r"\1=[凭据已隐藏]", text, ) return text def visible(value, key=""): if CREDENTIAL.search(str(key)): return "[凭据已隐藏]" if isinstance(value, dict): return {str(k): visible(v, k) for k, v in value.items()} if isinstance(value, (list, tuple)): return [visible(item) for item in value] return value def recent_lines(directory, limit=UI_LIMIT): """Read backwards, not the entire accumulated log history.""" rows = [] for path in sorted(directory.glob("????-??-??.log"), reverse=True): with path.open("rb") as stream: stream.seek(0, 2) position, carry = stream.tell(), b"" while position and len(rows) < limit: size = min(position, 65536) position -= size stream.seek(position) parts = (stream.read(size) + carry).split(b"\n") carry = parts.pop(0) for part in reversed(parts): if part and len(rows) < limit: rows.append(clean(part.decode("utf-8", errors="replace"))) if not position and carry and len(rows) < limit: rows.append(clean(carry.decode("utf-8", errors="replace"))) if len(rows) >= limit: break return list(reversed(rows)) class DailyLog(logging.Handler): def __init__(self, root): super().__init__() self.directory = Path(root) / "logs" self.entries = deque(maxlen=UI_LIMIT) self.sequence = 0 self.stream = None self.day = None self.file_error = "" self.setFormatter( logging.Formatter( "%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s", "%Y-%m-%d %H:%M:%S", ) ) self.logger = logging.Logger("douyin.activity", logging.INFO) self.logger.addHandler(self) try: for line in recent_lines(self.directory): self.remember(line) except OSError as exc: self.file_error = "历史日志读取失败:" + type(exc).__name__ self.remember("[ERROR] " + self.file_error) def remember(self, line): self.sequence += 1 self.entries.append((self.sequence, line)) def since(self, cursor): return [line for seq, line in self.entries if seq > cursor] def record(self, stage, message, level=logging.INFO, **fields): values = [] for key, value in fields.items(): if value is None: continue value = visible(value, key) if not isinstance(value, (str, int, float, bool)): value = json.dumps(value, ensure_ascii=False, default=str) values.append(f"{clean(key)}={clean(value)}") self.logger.log( level, "[%s] %s %s", clean(stage), clean(message), " ".join(values) ) def emit(self, record): line = clean(self.format(record)) self.remember(line) day = datetime.fromtimestamp(record.created).strftime("%Y-%m-%d") try: if self.day != day or self.stream is None: if self.stream: self.stream.close() self.stream = None self.directory.mkdir(parents=True, exist_ok=True, mode=0o700) self.stream = (self.directory / (day + ".log")).open( "a", encoding="utf-8", newline="\n" ) self.day = day self.stream.write(line + "\n") self.stream.flush() self.file_error = "" except OSError as exc: message = ( "日志文件写入失败(" + type(exc).__name__ + "),当前仅保留内存最近 2000 条,请检查磁盘与权限" ) if message != self.file_error: self.remember("[ERROR] " + message) self.file_error = message if self.stream: with suppress(OSError): self.stream.close() self.stream = None def close(self): if self.stream: with suppress(OSError): self.stream.close() self.stream = None self.logger.removeHandler(self) super().close()