lc_core/observability/
agent_metrics.rs1use crate::language_models::TokenUsage;
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct AgentMetrics {
15 pub trace_id: Option<String>,
17 pub llm_calls: usize,
19 pub cache_hits: usize,
21 pub tool_calls: usize,
23 pub total_tokens: Option<usize>,
25 pub duration: Duration,
27}
28
29impl AgentMetrics {
30 pub fn add_token_usage(&mut self, usage: &TokenUsage) {
32 self.total_tokens = Some(self.total_tokens.unwrap_or(0) + usage.total_tokens);
33 }
34
35 pub fn tokens_per_call(&self) -> Option<f64> {
37 if self.llm_calls == 0 {
38 return None;
39 }
40 self.total_tokens.map(|t| t as f64 / self.llm_calls as f64)
41 }
42
43 pub fn log_summary(&self) {
45 let trace = self.trace_id.as_deref().unwrap_or("-");
46 let duration_ms = self.duration.as_millis();
47 match self.total_tokens {
48 Some(tokens) => log::info!(
49 target: "lc_agents::metrics",
50 "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens={} duration_ms={}",
51 trace,
52 self.llm_calls,
53 self.tool_calls,
54 tokens,
55 duration_ms
56 ),
57 None => log::info!(
58 target: "lc_agents::metrics",
59 "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens=n/a duration_ms={}",
60 trace,
61 self.llm_calls,
62 self.tool_calls,
63 duration_ms
64 ),
65 }
66 }
67}
68
69#[cfg(test)]
70mod tests {
71 use super::*;
72
73 #[test]
74 fn test_metrics_default() {
75 let m = AgentMetrics::default();
76 assert_eq!(m.llm_calls, 0);
77 assert_eq!(m.tool_calls, 0);
78 assert_eq!(m.total_tokens, None);
79 assert_eq!(m.trace_id, None);
80 assert_eq!(m.duration, Duration::ZERO);
81 }
82
83 #[test]
84 fn test_add_token_usage() {
85 let mut m = AgentMetrics::default();
86 m.add_token_usage(&TokenUsage {
87 prompt_tokens: 10,
88 completion_tokens: 5,
89 total_tokens: 15,
90 });
91 m.add_token_usage(&TokenUsage {
92 prompt_tokens: 3,
93 completion_tokens: 2,
94 total_tokens: 5,
95 });
96 assert_eq!(m.total_tokens, Some(20));
97 }
98
99 #[test]
100 fn test_tokens_per_call() {
101 let mut m = AgentMetrics::default();
102 assert_eq!(m.tokens_per_call(), None);
103 m.llm_calls = 2;
104 m.total_tokens = Some(20);
105 assert_eq!(m.tokens_per_call(), Some(10.0));
106 }
107
108 #[test]
109 fn test_tokens_per_call_zero_calls() {
110 let m = AgentMetrics {
111 total_tokens: Some(10),
112 ..Default::default()
113 };
114 assert_eq!(m.tokens_per_call(), None);
115 }
116
117 #[test]
118 fn test_serde_roundtrip() {
119 let m = AgentMetrics {
120 trace_id: Some("trace-x".to_string()),
121 llm_calls: 3,
122 cache_hits: 1,
123 tool_calls: 2,
124 total_tokens: Some(42),
125 duration: Duration::from_millis(1500),
126 };
127 let json = serde_json::to_string(&m).expect("serialize");
128 let back: AgentMetrics = serde_json::from_str(&json).expect("deserialize");
129 assert_eq!(back.trace_id, m.trace_id);
130 assert_eq!(back.llm_calls, m.llm_calls);
131 assert_eq!(back.cache_hits, m.cache_hits);
132 assert_eq!(back.tool_calls, m.tool_calls);
133 assert_eq!(back.total_tokens, m.total_tokens);
134 assert_eq!(back.duration, m.duration);
135 }
136
137 #[test]
138 fn test_log_summary_no_panic() {
139 let m = AgentMetrics {
140 trace_id: Some("trace-x".to_string()),
141 ..Default::default()
142 };
143 m.log_summary();
144 let m = AgentMetrics {
145 total_tokens: Some(42),
146 ..Default::default()
147 };
148 m.log_summary();
149 }
150}