from __future__ import annotations import time from dataclasses import dataclass, field from datetime import datetime from typing import Any def aware_now() -> datetime: return datetime.now().astimezone() def iso(value: datetime) -> str: return value.isoformat(timespec="milliseconds") def parse_task_start(raw: str | None, process_start: datetime) -> tuple[datetime, str]: if raw is None: return process_start, "process_start" try: value = datetime.fromisoformat(raw) except ValueError as exc: raise ValueError("--task-start 必须是 ISO-8601 时间") from exc if value.tzinfo is None or value.utcoffset() is None: raise ValueError("--task-start 必须带时区") if value > process_start: raise ValueError("--task-start 不得晚于进程开始时间") return value, "arg" @dataclass class RunClock: process_start_wall: datetime = field(default_factory=aware_now) process_start_mono: float = field(default_factory=time.monotonic) task_start_wall: datetime | None = None task_start_source: str | None = None phases: dict[str, dict[str, Any]] = field(default_factory=dict) providers: list[dict[str, Any]] = field(default_factory=list) def set_task_start(self, raw: str | None) -> None: self.task_start_wall, self.task_start_source = parse_task_start( raw, self.process_start_wall ) def begin(self, name: str) -> None: self.phases[name] = { "started_at": iso(aware_now()), "started_mono": time.monotonic(), "status": "RUNNING", } def end(self, name: str, status: str = "OK") -> None: phase = self.phases[name] end_mono = time.monotonic() phase.update( { "finished_at": iso(aware_now()), "elapsed_seconds": max(0.0, end_mono - phase.pop("started_mono")), "status": status, } ) def walls(self, end_wall: datetime | None = None, end_mono: float | None = None) -> tuple[float, float]: end_wall = end_wall or aware_now() end_mono = end_mono if end_mono is not None else time.monotonic() process = max(0.0, end_mono - self.process_start_mono) assert self.task_start_wall is not None task = max(process, (end_wall - self.task_start_wall).total_seconds()) return task, process def metrics(self, run_id: str, status: str, commit_ready_at: datetime) -> dict[str, Any]: task, process = self.walls(commit_ready_at) return { "schema_version": 1, "run_id": run_id, "status": status, "task_start": iso(self.task_start_wall), "task_start_source": self.task_start_source, "process_start": iso(self.process_start_wall), "commit_ready_at": iso(commit_ready_at), "task_wall_seconds": task, "process_wall_seconds": process, "wall_scope": "through_commit_ready", "phases": self.phases, "providers": self.providers, }