|
|
@@ -0,0 +1,277 @@
|
|
|
+"""handler 共享日志工具 —— LogService 注入 + TaskLogger + trace_id 上下文
|
|
|
+
|
|
|
+镜像 _utils.py 的 get_db() 模式:app.py 启动时注入 LogService,handler 通过
|
|
|
+get_task_logger() 获得 TaskLogger 实例,自动管理 trace_id 和计时。
|
|
|
+
|
|
|
+trace_id 传递链路:
|
|
|
+ XxlJobExecutor.run() → _current_trace_id.set(...) → ContextVar
|
|
|
+ └─ handler(param) → get_task_logger() → 自动读取 ContextVar 中的 trace_id
|
|
|
+ └─ service.deal(..., task_logger=tlog) → 传入 TaskLogger
|
|
|
+"""
|
|
|
+
|
|
|
+from __future__ import annotations
|
|
|
+
|
|
|
+import contextvars
|
|
|
+import logging
|
|
|
+import time
|
|
|
+from typing import TYPE_CHECKING
|
|
|
+
|
|
|
+from src.infra.observability import (
|
|
|
+ LogCategory,
|
|
|
+ LogLevel,
|
|
|
+ LogStatus,
|
|
|
+ LogRecord,
|
|
|
+ TaskEvent,
|
|
|
+)
|
|
|
+from src.infra.shared.tools import generate_task_trace_id
|
|
|
+
|
|
|
+if TYPE_CHECKING:
|
|
|
+ from src.infra.observability.logging import LogService
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+# trace_id 上下文变量 —— 跨 handler/service 隐式穿透
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+_current_trace_id: contextvars.ContextVar[str] = contextvars.ContextVar(
|
|
|
+ "task_trace_id", default=""
|
|
|
+)
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+# LogService 注入槽 —— app.py 启动时注入
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+_log_service_ref: "LogService | None" = None
|
|
|
+
|
|
|
+
|
|
|
+def set_log_service(ls: "LogService") -> None:
|
|
|
+ """启动时注入 LogService,与 set_db() 对称"""
|
|
|
+ global _log_service_ref
|
|
|
+ _log_service_ref = ls
|
|
|
+
|
|
|
+
|
|
|
+def get_log_service() -> "LogService":
|
|
|
+ """运行时获取已注入的 LogService"""
|
|
|
+ if _log_service_ref is None:
|
|
|
+ raise RuntimeError(
|
|
|
+ "LogService 尚未注入,请确认 app.py 中已调用 set_log_service()"
|
|
|
+ )
|
|
|
+ return _log_service_ref
|
|
|
+
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+# TaskLogger —— 任务级日志工具
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+
|
|
|
+class TaskLogger:
|
|
|
+ """任务日志器 —— 封装 LogService 提供业务友好的结构化日志 API
|
|
|
+
|
|
|
+ 自动管理 trace_id、计时、事件标准化。每个 handler 实例创建一个。
|
|
|
+
|
|
|
+ 使用方式:
|
|
|
+ tlog = get_task_logger("articleSearch")
|
|
|
+ await tlog.task_started(dt="20260701", limit=2000)
|
|
|
+ try:
|
|
|
+ ...
|
|
|
+ await tlog.phase_start("search")
|
|
|
+ ...
|
|
|
+ await tlog.phase_end("search", items=100)
|
|
|
+ await tlog.task_completed(processed=100)
|
|
|
+ except Exception as e:
|
|
|
+ await tlog.task_failed(e)
|
|
|
+ """
|
|
|
+
|
|
|
+ def __init__(
|
|
|
+ self,
|
|
|
+ log_service: "LogService",
|
|
|
+ task_name: str,
|
|
|
+ trace_id: str | None = None,
|
|
|
+ ):
|
|
|
+ self._ls = log_service
|
|
|
+ self._task_name = task_name
|
|
|
+ self.trace_id = trace_id or _current_trace_id.get() or generate_task_trace_id()
|
|
|
+ self._started_at = time.monotonic()
|
|
|
+ self._phase_timers: dict[str, float] = {}
|
|
|
+
|
|
|
+ # ── 生命周期 ──
|
|
|
+
|
|
|
+ async def task_started(self, message: str = "", **extra) -> None:
|
|
|
+ """任务开始 —— 在 handler 入口调用一次"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=TaskEvent.TASK_STARTED,
|
|
|
+ message=message or f"{self._task_name} 开始执行",
|
|
|
+ status=LogStatus.PENDING,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def task_completed(self, message: str = "", **extra) -> None:
|
|
|
+ """任务完成 —— handler 正常结束时调用"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=TaskEvent.TASK_COMPLETED,
|
|
|
+ message=message or f"{self._task_name} 执行完成",
|
|
|
+ status=LogStatus.SUCCESS,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def task_failed(self, error: Exception | None = None, message: str = "", **extra) -> None:
|
|
|
+ """任务失败 —— handler 异常时调用"""
|
|
|
+ if not message:
|
|
|
+ message = str(error) if error else f"{self._task_name} 执行失败"
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.ERROR,
|
|
|
+ event=TaskEvent.TASK_FAILED,
|
|
|
+ message=message,
|
|
|
+ status=LogStatus.FAILED,
|
|
|
+ error_type=type(error).__name__ if error else "",
|
|
|
+ error_message=str(error) if error else "",
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ # ── 阶段标记 ──
|
|
|
+
|
|
|
+ async def phase_start(self, phase: str, message: str = "", **extra) -> None:
|
|
|
+ """阶段开始 —— 标记管道阶段"""
|
|
|
+ self._phase_timers[phase] = time.monotonic()
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=TaskEvent.PHASE_START,
|
|
|
+ phase=phase,
|
|
|
+ message=message or f"阶段开始: {phase}",
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def phase_end(self, phase: str, message: str = "", **extra) -> None:
|
|
|
+ """阶段结束 —— 自动计算阶段耗时"""
|
|
|
+ phase_start = self._phase_timers.pop(phase, self._started_at)
|
|
|
+ phase_duration = int((time.monotonic() - phase_start) * 1000)
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=TaskEvent.PHASE_END,
|
|
|
+ phase=phase,
|
|
|
+ message=message or f"阶段结束: {phase}",
|
|
|
+ duration_ms=phase_duration,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ # ── 业务事件 ──
|
|
|
+
|
|
|
+ async def info(
|
|
|
+ self,
|
|
|
+ event: str | TaskEvent,
|
|
|
+ message: str = "",
|
|
|
+ phase: str = "",
|
|
|
+ **extra,
|
|
|
+ ) -> None:
|
|
|
+ """通用信息日志"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=event if isinstance(event, str) else event.value,
|
|
|
+ phase=phase,
|
|
|
+ message=message,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def warn(
|
|
|
+ self,
|
|
|
+ event: str | TaskEvent,
|
|
|
+ message: str = "",
|
|
|
+ phase: str = "",
|
|
|
+ **extra,
|
|
|
+ ) -> None:
|
|
|
+ """警告日志"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.WARN,
|
|
|
+ event=event if isinstance(event, str) else event.value,
|
|
|
+ phase=phase,
|
|
|
+ message=message,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def error(
|
|
|
+ self,
|
|
|
+ event: str | TaskEvent,
|
|
|
+ message: str = "",
|
|
|
+ exc: Exception | None = None,
|
|
|
+ phase: str = "",
|
|
|
+ **extra,
|
|
|
+ ) -> None:
|
|
|
+ """错误日志"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.ERROR,
|
|
|
+ event=event if isinstance(event, str) else event.value,
|
|
|
+ phase=phase,
|
|
|
+ message=message or (str(exc) if exc else ""),
|
|
|
+ error_type=type(exc).__name__ if exc else "",
|
|
|
+ error_message=str(exc) if exc else "",
|
|
|
+ status=LogStatus.FAILED,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ async def item_processed(
|
|
|
+ self,
|
|
|
+ event: str | TaskEvent,
|
|
|
+ message: str = "",
|
|
|
+ phase: str = "",
|
|
|
+ successful: bool = True,
|
|
|
+ **extra,
|
|
|
+ ) -> None:
|
|
|
+ """单条处理结果"""
|
|
|
+ await self._emit(
|
|
|
+ level=LogLevel.INFO,
|
|
|
+ event=event if isinstance(event, str) else event.value,
|
|
|
+ phase=phase,
|
|
|
+ message=message,
|
|
|
+ status=LogStatus.SUCCESS if successful else LogStatus.FAILED,
|
|
|
+ **extra,
|
|
|
+ )
|
|
|
+
|
|
|
+ # ── 内部方法 ──
|
|
|
+
|
|
|
+ async def _emit(
|
|
|
+ self,
|
|
|
+ level: LogLevel,
|
|
|
+ event: str,
|
|
|
+ message: str = "",
|
|
|
+ phase: str = "",
|
|
|
+ status: LogStatus = LogStatus.SUCCESS,
|
|
|
+ duration_ms: int = 0,
|
|
|
+ error_type: str = "",
|
|
|
+ error_message: str = "",
|
|
|
+ **extra,
|
|
|
+ ) -> None:
|
|
|
+ """构造 LogRecord 并推送到 SLS"""
|
|
|
+ total_elapsed = int((time.monotonic() - self._started_at) * 1000)
|
|
|
+ record = LogRecord(
|
|
|
+ level=level,
|
|
|
+ category=LogCategory.TASK,
|
|
|
+ event=event,
|
|
|
+ message=message,
|
|
|
+ trace_id=self.trace_id,
|
|
|
+ task_name=self._task_name,
|
|
|
+ phase=phase,
|
|
|
+ duration_ms=duration_ms or total_elapsed,
|
|
|
+ status=status,
|
|
|
+ error_type=error_type,
|
|
|
+ error_message=error_message,
|
|
|
+ extra=extra,
|
|
|
+ )
|
|
|
+ await self._ls.log(record)
|
|
|
+
|
|
|
+
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+# 便捷工厂
|
|
|
+# ═══════════════════════════════════════════════════════════════
|
|
|
+
|
|
|
+
|
|
|
+def get_task_logger(task_name: str, trace_id: str | None = None) -> TaskLogger:
|
|
|
+ """从注入的 LogService 创建 TaskLogger —— handler 入口一行获取
|
|
|
+
|
|
|
+ 使用方式:
|
|
|
+ from src.handlers._logging import get_task_logger
|
|
|
+ tlog = get_task_logger("articleSearch")
|
|
|
+ """
|
|
|
+ return TaskLogger(get_log_service(), task_name, trace_id=trace_id)
|