"""任务领取到商品页的脱敏分阶段计时。 日志字段使用白名单,只包含任务编号、稳定操作名、毫秒耗时和结果分类。 不要把 URL、控件树、异常正文或业务数据传入本模块。 """ from __future__ import annotations import json import logging from contextlib import contextmanager from contextvars import ContextVar from logging.handlers import RotatingFileHandler from pathlib import Path import time from typing import Callable, Iterator, Optional from .db import data_dir PERFORMANCE_LOG_NAME = "task_performance.jsonl" _LOGGER_NAME = "cmautobuy.performance" _ACTIVE_TRACE: ContextVar[Optional["TaskPerformanceTrace"]] = ContextVar( "cmautobuy_task_performance_trace", default=None ) def _default_sink(record: dict[str, object]) -> None: logging.getLogger(_LOGGER_NAME).info( json.dumps(record, ensure_ascii=False, separators=(",", ":")) ) def configure_performance_logging( directory: Optional[Path] = None, ) -> Path: """把性能事件写入可轮转 JSONL 文件,重复调用不会增加处理器。""" log_directory = directory or data_dir() / "logs" log_directory.mkdir(parents=True, exist_ok=True) log_path = log_directory / PERFORMANCE_LOG_NAME logger = logging.getLogger(_LOGGER_NAME) if not any( getattr(handler, "baseFilename", None) == str(log_path.resolve()) for handler in logger.handlers ): handler = RotatingFileHandler( log_path, maxBytes=2 * 1024 * 1024, backupCount=3, encoding="utf-8", ) handler.setFormatter(logging.Formatter("%(message)s")) logger.addHandler(handler) logger.setLevel(logging.INFO) logger.propagate = False return log_path class TaskPerformanceTrace: """收集一条任务的阶段耗时,并在绑定任务编号后输出记录。""" def __init__( self, *, monotonic: Callable[[], float] = time.monotonic, sink: Callable[[dict[str, object]], None] = _default_sink, ) -> None: self._monotonic = monotonic self._sink = sink self._started_at = monotonic() self._task_id = "" self._pending: list[tuple[str, int, str]] = [] @property def task_id(self) -> str: return self._task_id def bind_task(self, task_id: str) -> None: """绑定稳定任务编号,并输出绑定前缓存的领取阶段记录。""" value = str(task_id or "").strip() if not value: return if self._task_id and self._task_id != value: raise RuntimeError("性能计时不能跨任务复用") self._task_id = value pending, self._pending = self._pending, [] for operation, duration_ms, result in pending: self._emit(operation, duration_ms, result) @contextmanager def stage(self, operation: str) -> Iterator[None]: """记录一个阶段;异常时只写 failed,不记录异常正文。""" started_at = self._monotonic() try: yield except BaseException: self.record(operation, self._monotonic() - started_at, "failed") raise else: self.record(operation, self._monotonic() - started_at, "succeeded") def record(self, operation: str, seconds: float, result: str) -> None: """记录白名单阶段数据,负耗时会安全归零。""" checked_operation = str(operation or "").strip() checked_result = str(result or "").strip() if not checked_operation or not checked_result: raise ValueError("性能操作名和结果分类不能为空") duration_ms = max(0, round(float(seconds) * 1000)) if self._task_id: self._emit(checked_operation, duration_ms, checked_result) else: self._pending.append( (checked_operation, duration_ms, checked_result) ) def checkpoint(self, operation: str, result: str = "succeeded") -> None: """记录从本轮调度开始到当前时刻的端到端耗时。""" self.record(operation, self._monotonic() - self._started_at, result) @contextmanager def activate(self) -> Iterator["TaskPerformanceTrace"]: """让同一工作线程内的设备和页面适配层取得本计时对象。""" token = _ACTIVE_TRACE.set(self) try: yield self finally: _ACTIVE_TRACE.reset(token) def _emit(self, operation: str, duration_ms: int, result: str) -> None: self._sink( { "task_id": self._task_id, "operation": operation, "duration_ms": duration_ms, "result": result, } ) def current_performance_trace() -> Optional[TaskPerformanceTrace]: """返回当前工作线程的任务计时对象;普通调用返回 None。""" return _ACTIVE_TRACE.get()