Coverage for agentos/observability/tracer.py: 58%
81 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-06 10:59 +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 """单步追踪记录。"""
16 name: str
17 start_time: float = 0.0
18 end_time: float = 0.0
19 duration_ms: float = 0.0
20 model: str = ""
21 tokens_in: int = 0
22 tokens_out: int = 0
23 tool_calls_count: int = 0
24 error: str | None = None
27@dataclass
28class TokenStats:
29 """Token 使用统计。"""
30 total_input: int = 0
31 total_output: int = 0
32 by_model: dict[str, dict[str, int]] = field(default_factory=dict)
35@dataclass
36class ObservabilityReport:
37 """可观测性报告。"""
38 session_id: str
39 total_duration_ms: float = 0.0
40 steps: list[StepTrace] = field(default_factory=list)
41 tokens: TokenStats = field(default_factory=TokenStats)
42 model_calls: int = 0
43 tool_calls_total: int = 0
45 def summary(self) -> str:
46 lines = [
47 f"Session: {self.session_id}",
48 f"Duration: {self.total_duration_ms:.0f}ms",
49 f"Model calls: {self.model_calls}",
50 f"Tool calls: {self.tool_calls_total}",
51 f"Tokens (in/out): {self.tokens.total_input}/{self.tokens.total_output}",
52 ]
53 if self.steps:
54 lines.append(f"Steps: {', '.join(s.name for s in self.steps)}")
55 return "\n".join(lines)
58class Tracer:
59 """全链路追踪器。每步记录耗时、token消耗、工具调用。"""
61 def __init__(self, session_id: str = ""):
62 self.session_id = session_id
63 self.steps: list[StepTrace] = []
64 self.token_stats = TokenStats()
65 self.start_time = time.time()
67 @classmethod
68 def noop(cls) -> "Tracer":
69 return NoopTracer()
71 @contextmanager
72 def step(self, name: str, model: str = ""):
73 trace = StepTrace(name=name, model=model, start_time=time.time())
74 try:
75 yield trace
76 except Exception as e:
77 trace.error = str(e)
78 raise
79 finally:
80 trace.end_time = time.time()
81 trace.duration_ms = (trace.end_time - trace.start_time) * 1000
82 self.steps.append(trace)
84 def track_tokens(self, model: str, input_tokens: int, output_tokens: int):
85 self.token_stats.total_input += input_tokens
86 self.token_stats.total_output += output_tokens
87 if model not in self.token_stats.by_model:
88 self.token_stats.by_model[model] = {"input": 0, "output": 0}
89 self.token_stats.by_model[model]["input"] += input_tokens
90 self.token_stats.by_model[model]["output"] += output_tokens
92 def track_tool_call(self):
93 if self.steps:
94 self.steps[-1].tool_calls_count += 1
96 def report(self) -> ObservabilityReport:
97 return ObservabilityReport(
98 session_id=self.session_id,
99 total_duration_ms=(time.time() - self.start_time) * 1000,
100 steps=self.steps,
101 tokens=self.token_stats,
102 model_calls=len(self.steps),
103 tool_calls_total=sum(s.tool_calls_count for s in self.steps),
104 )
106 def token_summary(self) -> dict[str, int]:
107 return {
108 "input": self.token_stats.total_input,
109 "output": self.token_stats.total_output,
110 }
113class NoopTracer(Tracer):
114 """空追踪器 — 生产环境中关闭追踪时使用。"""
116 def __init__(self):
117 pass
119 @contextmanager
120 def step(self, name: str, model: str = ""):
121 yield StepTrace(name=name)
123 def track_tokens(self, model: str, input_tokens: int, output_tokens: int):
124 pass
126 def track_tool_call(self):
127 pass
129 def report(self) -> ObservabilityReport:
130 return ObservabilityReport(session_id="noop")
132 def token_summary(self) -> dict[str, int]:
133 return {}