Coverage for agentos/observability/tracer.py: 58%
81 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 19:15 +0800
1"""
2全链路追踪 — 每一步可追溯。
3基因来源: LangSmith + OpenAI Tracing
4"""
6from __future__ import annotations
8import time
9from contextlib import contextmanager
10from dataclasses import dataclass, field
13@dataclass
14class StepTrace:
15 """单步追踪记录。"""
17 name: str
18 start_time: float = 0.0
19 end_time: float = 0.0
20 duration_ms: float = 0.0
21 model: str = ""
22 tokens_in: int = 0
23 tokens_out: int = 0
24 tool_calls_count: int = 0
25 error: str | None = None
28@dataclass
29class TokenStats:
30 """Token 使用统计。"""
32 total_input: int = 0
33 total_output: int = 0
34 by_model: dict[str, dict[str, int]] = field(default_factory=dict)
37@dataclass
38class ObservabilityReport:
39 """可观测性报告。"""
41 session_id: str
42 total_duration_ms: float = 0.0
43 steps: list[StepTrace] = field(default_factory=list)
44 tokens: TokenStats = field(default_factory=TokenStats)
45 model_calls: int = 0
46 tool_calls_total: int = 0
48 def summary(self) -> str:
49 lines = [
50 f"Session: {self.session_id}",
51 f"Duration: {self.total_duration_ms:.0f}ms",
52 f"Model calls: {self.model_calls}",
53 f"Tool calls: {self.tool_calls_total}",
54 f"Tokens (in/out): {self.tokens.total_input}/{self.tokens.total_output}",
55 ]
56 if self.steps:
57 lines.append(f"Steps: {', '.join(s.name for s in self.steps)}")
58 return "\n".join(lines)
61class Tracer:
62 """全链路追踪器。每步记录耗时、token消耗、工具调用。"""
64 def __init__(self, session_id: str = ""):
65 self.session_id = session_id
66 self.steps: list[StepTrace] = []
67 self.token_stats = TokenStats()
68 self.start_time = time.time()
70 @classmethod
71 def noop(cls) -> Tracer:
72 return NoopTracer()
74 @contextmanager
75 def step(self, name: str, model: str = ""):
76 trace = StepTrace(name=name, model=model, start_time=time.time())
77 try:
78 yield trace
79 except Exception as e:
80 trace.error = str(e)
81 raise
82 finally:
83 trace.end_time = time.time()
84 trace.duration_ms = (trace.end_time - trace.start_time) * 1000
85 self.steps.append(trace)
87 def track_tokens(self, model: str, input_tokens: int, output_tokens: int):
88 self.token_stats.total_input += input_tokens
89 self.token_stats.total_output += output_tokens
90 if model not in self.token_stats.by_model:
91 self.token_stats.by_model[model] = {"input": 0, "output": 0}
92 self.token_stats.by_model[model]["input"] += input_tokens
93 self.token_stats.by_model[model]["output"] += output_tokens
95 def track_tool_call(self):
96 if self.steps:
97 self.steps[-1].tool_calls_count += 1
99 def report(self) -> ObservabilityReport:
100 return ObservabilityReport(
101 session_id=self.session_id,
102 total_duration_ms=(time.time() - self.start_time) * 1000,
103 steps=self.steps,
104 tokens=self.token_stats,
105 model_calls=len(self.steps),
106 tool_calls_total=sum(s.tool_calls_count for s in self.steps),
107 )
109 def token_summary(self) -> dict[str, int]:
110 return {
111 "input": self.token_stats.total_input,
112 "output": self.token_stats.total_output,
113 }
116class NoopTracer(Tracer):
117 """空追踪器 — 生产环境中关闭追踪时使用。"""
119 def __init__(self):
120 pass
122 @contextmanager
123 def step(self, name: str, model: str = ""):
124 yield StepTrace(name=name)
126 def track_tokens(self, model: str, input_tokens: int, output_tokens: int):
127 pass
129 def track_tool_call(self):
130 pass
132 def report(self) -> ObservabilityReport:
133 return ObservabilityReport(session_id="noop")
135 def token_summary(self) -> dict[str, int]:
136 return {}