phi_telemetry/
collector.rs1use agent_base::TurnContext;
7use serde_json::Value;
8use std::sync::{Arc, Mutex};
9use tokio::sync::RwLock;
10use tokio::sync::mpsc;
11use tokio::task::JoinHandle;
12use tracing;
13
14use crate::types::{SessionMetrics, TurnMetrics, run_outcome_to_turn_outcome};
15
16#[derive(Clone, Debug)]
18pub(crate) enum ObserverMsg {
19 TurnEnd(TurnContext),
21 SetSessionCustom(Value),
23 Shutdown,
25}
26
27pub struct ObserverHandle {
33 tx: mpsc::UnboundedSender<ObserverMsg>,
34 pub session: Arc<RwLock<SessionMetrics>>,
36 task: Option<JoinHandle<()>>,
38 pending_turn_custom: Arc<Mutex<Option<Value>>>,
41}
42
43impl ObserverHandle {
44 pub fn set_session_custom(&self, custom: Value) {
46 let _ = self.tx.send(ObserverMsg::SetSessionCustom(custom));
47 }
48
49 pub fn set_turn_custom(&self, custom: Value) {
60 if let Ok(mut pending) = self.pending_turn_custom.lock() {
61 *pending = Some(custom);
62 }
63 }
64
65 pub async fn shutdown(&mut self) {
72 let _ = self.tx.send(ObserverMsg::Shutdown);
73 if let Some(task) = self.task.take() {
74 let _ = task.await;
76 }
77 }
78}
79
80pub fn init_telemetry(
90 runtime: &agent_base::AgentRuntime,
91 session_id: String,
92 node_id: String,
93 model: String,
94) -> ObserverHandle {
95 let (tx, mut rx) = mpsc::unbounded_channel::<ObserverMsg>();
96
97 let hook_tx = tx.clone();
99 runtime.on_turn_end(move |ctx: &TurnContext| {
100 let _ = hook_tx.send(ObserverMsg::TurnEnd(ctx.clone()));
101 });
102
103 let session = Arc::new(RwLock::new(SessionMetrics::new(session_id, node_id, model)));
104
105 let observer_session = session.clone();
106 let pending_turn_custom = Arc::new(Mutex::new(None::<Value>));
107 let pending = pending_turn_custom.clone();
108
109 let task = tokio::spawn(async move {
111 let accumulator = observer_session;
112 while let Some(msg) = rx.recv().await {
113 match msg {
114 ObserverMsg::TurnEnd(ctx) => {
115 let turn = {
116 let mut turn = build_turn_metrics(&ctx);
117 if let Ok(mut pending) = pending.lock() {
119 if let Some(custom) = pending.take() {
120 if let Value::Object(ref mut map) = turn.custom {
121 if let Value::Object(custom_map) = custom {
122 for (k, v) in custom_map {
123 map.insert(k, v);
124 }
125 }
126 }
127 }
128 }
129 turn
130 };
131 let mut session = accumulator.write().await;
132 session.append_turn(turn);
133 tracing::debug!(turn = session.total_turns, "metrics: turn accumulated");
134 }
135 ObserverMsg::SetSessionCustom(custom) => {
136 let mut session = accumulator.write().await;
137 if let Value::Object(ref mut map) = session.custom
138 && let Value::Object(custom_map) = custom
139 {
140 for (k, v) in custom_map {
141 map.insert(k, v);
142 }
143 }
144 }
145 ObserverMsg::Shutdown => {
146 tracing::debug!("metrics: observer shutting down");
147 break;
148 }
149 }
150 }
151 });
152
153 ObserverHandle {
154 tx,
155 session,
156 task: Some(task),
157 pending_turn_custom,
158 }
159}
160
161fn build_turn_metrics(ctx: &TurnContext) -> TurnMetrics {
163 let input_tokens = ctx
164 .usage
165 .as_ref()
166 .and_then(|u| u.prompt_tokens)
167 .unwrap_or(0) as u64;
168 let output_tokens = ctx
169 .usage
170 .as_ref()
171 .and_then(|u| u.completion_tokens)
172 .unwrap_or(0) as u64;
173
174 let duration_ms = ctx.duration_ms;
175
176 let turn_outcome = run_outcome_to_turn_outcome(&ctx.outcome, &ctx.tools_used);
177
178 let mut turn = TurnMetrics::new(
179 ctx.turn_number,
180 chrono::Utc::now().to_rfc3339(),
181 duration_ms,
182 ctx.model.clone(),
183 ctx.user_input.clone(),
184 turn_outcome,
185 );
186
187 turn.time_to_first_token_ms = ctx.ttft_ms;
188 turn.llm_duration_ms = ctx.llm_duration_ms;
189 turn.tool_duration_ms = ctx.tool_duration_ms;
190 turn.input_tokens = input_tokens;
191 turn.output_tokens = output_tokens;
192 turn.tool_call_count = ctx.tool_call_count;
193 turn.tools_used = ctx.tools_used.clone();
194 turn.tool_success = ctx.tool_success;
195 turn.tool_failed = ctx.tool_failed;
196 turn.text_length = ctx.full_text_len;
197 turn.has_thinking = ctx.has_thinking;
198 turn.plan_updates = ctx.plan_updates;
199 turn.approval_count = ctx.approval_count;
200 turn.llm_calls = ctx.llm_calls;
201 if let Some(ref msg) = ctx.error_message {
202 turn.error_message = Some(msg.clone());
203 }
204
205 turn
206}