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 #[serde(default)]
26 pub compactions: usize,
27 pub total_tokens: Option<usize>,
29 pub duration: Duration,
31}
32
33impl AgentMetrics {
34 pub fn add_token_usage(&mut self, usage: &TokenUsage) {
36 self.total_tokens = Some(self.total_tokens.unwrap_or(0) + usage.total_tokens);
37 }
38
39 pub fn tokens_per_call(&self) -> Option<f64> {
41 if self.llm_calls == 0 {
42 return None;
43 }
44 self.total_tokens.map(|t| t as f64 / self.llm_calls as f64)
45 }
46
47 pub fn log_summary(&self) {
49 let trace = self.trace_id.as_deref().unwrap_or("-");
50 let duration_ms = self.duration.as_millis();
51 match self.total_tokens {
52 Some(tokens) => log::info!(
53 target: "lc_agents::metrics",
54 "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens={} duration_ms={}",
55 trace,
56 self.llm_calls,
57 self.tool_calls,
58 tokens,
59 duration_ms
60 ),
61 None => log::info!(
62 target: "lc_agents::metrics",
63 "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens=n/a duration_ms={}",
64 trace,
65 self.llm_calls,
66 self.tool_calls,
67 duration_ms
68 ),
69 }
70 }
71}
72
73#[cfg(test)]
74mod tests {
75 use super::*;
76
77 #[test]
78 fn test_metrics_default() {
79 let m = AgentMetrics::default();
80 assert_eq!(m.llm_calls, 0);
81 assert_eq!(m.tool_calls, 0);
82 assert_eq!(m.total_tokens, None);
83 assert_eq!(m.trace_id, None);
84 assert_eq!(m.duration, Duration::ZERO);
85 }
86
87 #[test]
88 fn test_add_token_usage() {
89 let mut m = AgentMetrics::default();
90 m.add_token_usage(&TokenUsage {
91 prompt_tokens: 10,
92 completion_tokens: 5,
93 total_tokens: 15,
94 });
95 m.add_token_usage(&TokenUsage {
96 prompt_tokens: 3,
97 completion_tokens: 2,
98 total_tokens: 5,
99 });
100 assert_eq!(m.total_tokens, Some(20));
101 }
102
103 #[test]
104 fn test_tokens_per_call() {
105 let mut m = AgentMetrics::default();
106 assert_eq!(m.tokens_per_call(), None);
107 m.llm_calls = 2;
108 m.total_tokens = Some(20);
109 assert_eq!(m.tokens_per_call(), Some(10.0));
110 }
111
112 #[test]
113 fn test_tokens_per_call_zero_calls() {
114 let m = AgentMetrics {
115 total_tokens: Some(10),
116 ..Default::default()
117 };
118 assert_eq!(m.tokens_per_call(), None);
119 }
120
121 #[test]
122 fn test_serde_roundtrip() {
123 let m = AgentMetrics {
124 trace_id: Some("trace-x".to_string()),
125 llm_calls: 3,
126 cache_hits: 1,
127 tool_calls: 2,
128 compactions: 1,
129 total_tokens: Some(42),
130 duration: Duration::from_millis(1500),
131 };
132 let json = serde_json::to_string(&m).expect("serialize");
133 let back: AgentMetrics = serde_json::from_str(&json).expect("deserialize");
134 assert_eq!(back.trace_id, m.trace_id);
135 assert_eq!(back.llm_calls, m.llm_calls);
136 assert_eq!(back.cache_hits, m.cache_hits);
137 assert_eq!(back.tool_calls, m.tool_calls);
138 assert_eq!(back.compactions, m.compactions);
139 assert_eq!(back.total_tokens, m.total_tokens);
140 assert_eq!(back.duration, m.duration);
141 }
142
143 #[test]
146 fn test_serde_roundtrip_without_compactions_field() {
147 let legacy = r#"{
148 "trace_id": "trace-x",
149 "llm_calls": 3,
150 "cache_hits": 1,
151 "tool_calls": 2,
152 "total_tokens": 42,
153 "duration": {"secs": 1, "nanos": 500000000}
154 }"#;
155 let back: AgentMetrics = serde_json::from_str(legacy).expect("legacy payload deserializes");
156 assert_eq!(back.compactions, 0, "missing field defaults to 0");
157 }
158
159 #[test]
160 fn test_log_summary_no_panic() {
161 let m = AgentMetrics {
162 trace_id: Some("trace-x".to_string()),
163 ..Default::default()
164 };
165 m.log_summary();
166 let m = AgentMetrics {
167 total_tokens: Some(42),
168 ..Default::default()
169 };
170 m.log_summary();
171 }
172}