ForcePilot/backend/package/yuxi/channel/runtime/trace.py
Kris d52e9b518d feat(channel/runtime): 新增链路追踪与启动运行时工具集
新增trace_ctx实现链路ID上下文管理与日志注入,添加boot.py启动管理器、startup.py启动任务执行器、backoff.py退避策略工具以及trace.py启动链路追踪模块,完善channel运行时基础能力
2026-05-21 10:28:46 +08:00

159 lines
4.8 KiB
Python

from __future__ import annotations
import time as _time
from dataclasses import dataclass, field
from enum import StrEnum
from typing import Any
class PhaseStatus(StrEnum):
PENDING = "pending"
RUNNING = "running"
OK = "ok"
FAILED = "failed"
SKIPPED = "skipped"
class GatewayReadyState(StrEnum):
NOT_READY = "not_ready"
READY = "ready"
DEGRADED = "degraded"
FAILED = "failed"
@dataclass
class PhaseMark:
name: str
status: PhaseStatus = PhaseStatus.PENDING
started_at: float | None = None
finished_at: float | None = None
@property
def elapsed_ms(self) -> float | None:
if self.started_at and self.finished_at:
return (self.finished_at - self.started_at) * 1000
return None
@dataclass
class StartupTrace:
"""启动追踪器 — 记录系统启动各阶段耗时。"""
started_at: float = 0.0
completed_at: float = 0.0
failed_at: float = 0.0
error: str | None = None
phases: list[dict[str, Any]] = field(default_factory=list)
_phase_marks: list[PhaseMark] = field(default_factory=list, repr=False)
_active_phase: str | None = field(default=None, repr=False)
ready_state: GatewayReadyState = GatewayReadyState.NOT_READY
trace_id: str = ""
def start(self) -> None:
self.started_at = _time.time()
self.phases.clear()
def add_phase(self, name: str, duration_ms: float, *, success: bool = True, error: str | None = None) -> None:
self.phases.append(
{
"name": name,
"duration_ms": round(duration_ms, 2),
"success": success,
"error": error,
}
)
def complete(self) -> None:
self.completed_at = _time.time()
def fail(self, error: str) -> None:
self.failed_at = _time.time()
self.error = error
@property
def duration(self) -> float:
if self.completed_at:
return self.completed_at - self.started_at
if self.failed_at:
return self.failed_at - self.started_at
return _time.time() - self.started_at
@property
def success(self) -> bool:
return self.completed_at > 0 and self.failed_at == 0
def to_dict(self) -> dict[str, Any]:
return {
"trace_id": self.trace_id,
"started_at": self.started_at,
"completed_at": self.completed_at,
"failed_at": self.failed_at,
"duration_seconds": round(self.duration, 3),
"success": self.success,
"ready_state": self.ready_state.value,
"error": self.error,
"phases": list(self.phases),
}
def start_phase(self, name: str) -> None:
mark = PhaseMark(name=name, status=PhaseStatus.RUNNING, started_at=_time.time())
self._upsert_mark(mark)
self._active_phase = name
def complete_phase(self, name: str, ok: bool = True, error: str | None = None) -> None:
mark = self._find_mark(name)
if mark is None:
return
mark.status = PhaseStatus.OK if ok else PhaseStatus.FAILED
mark.finished_at = _time.time()
self._active_phase = None
duration_ms = mark.elapsed_ms or 0
self.add_phase(name, duration_ms, success=ok, error=error)
def skip_phase(self, name: str) -> None:
mark = self._find_mark(name)
if mark is not None:
mark.status = PhaseStatus.SKIPPED
else:
mark = PhaseMark(name=name, status=PhaseStatus.SKIPPED)
self._phase_marks.append(mark)
self._active_phase = None
def finalize(self, ready_state: GatewayReadyState) -> None:
self.ready_state = ready_state
if not self.completed_at and not self.failed_at:
self.completed_at = _time.time()
def summary(self) -> dict[str, Any]:
return {
"trace_id": self.trace_id,
"ready_state": self.ready_state.value,
"total_elapsed_ms": round(self.duration * 1000, 2) if self.duration else None,
"phases": [
{
"name": p.name,
"status": p.status.value,
"elapsed_ms": round(p.elapsed_ms, 2) if p.elapsed_ms else None,
}
for p in self._phase_marks
],
"errors": [
e["error"] for e in self.phases if not e.get("success") and e.get("error")
],
}
def _upsert_mark(self, mark: PhaseMark) -> None:
existing = self._find_mark(mark.name)
if existing:
idx = self._phase_marks.index(existing)
self._phase_marks[idx] = mark
else:
self._phase_marks.append(mark)
def _find_mark(self, name: str) -> PhaseMark | None:
for p in self._phase_marks:
if p.name == name:
return p
return None