1use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10use std::sync::{Arc, OnceLock};
11use tokio::sync::Mutex;
12
13static GLOBAL_TRACKER: OnceLock<Arc<RtkTracker>> = OnceLock::new();
15
16pub fn global_tracker() -> Arc<RtkTracker> {
18 GLOBAL_TRACKER
19 .get_or_init(|| Arc::new(RtkTracker::new()))
20 .clone()
21}
22
23#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct TokenSavings {
26 pub command: String,
28 pub rewritten_command: String,
30 pub original_tokens: usize,
32 pub filtered_tokens: usize,
34 pub tokens_saved: usize,
36 pub savings_percent: f64,
38 pub timestamp: DateTime<Utc>,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct RtkMetrics {
45 pub total_commands: usize,
47 pub total_tokens_saved: usize,
49 pub average_savings_percent: f64,
51 pub savings_by_command: HashMap<String, CommandSavings>,
53 pub recent_savings: Vec<TokenSavings>,
55 pub tracking_since: DateTime<Utc>,
57}
58
59#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct CommandSavings {
62 pub execution_count: usize,
64 pub total_tokens_saved: usize,
66 pub average_savings_percent: f64,
68}
69
70#[derive(Debug, Clone)]
72pub struct RtkTracker {
73 metrics: Arc<Mutex<RtkMetrics>>,
74}
75
76impl RtkTracker {
77 pub fn new() -> Self {
79 Self {
80 metrics: Arc::new(Mutex::new(RtkMetrics {
81 total_commands: 0,
82 total_tokens_saved: 0,
83 average_savings_percent: 0.0,
84 savings_by_command: HashMap::new(),
85 recent_savings: Vec::new(),
86 tracking_since: Utc::now(),
87 })),
88 }
89 }
90
91 pub async fn record_savings(&self, savings: TokenSavings) {
102 let mut metrics = self.metrics.lock().await;
103
104 metrics.total_commands += 1;
106 metrics.total_tokens_saved += savings.tokens_saved;
107
108 let total_percent: f64 = metrics
110 .recent_savings
111 .iter()
112 .map(|s| s.savings_percent)
113 .sum::<f64>()
114 + savings.savings_percent;
115 let count = metrics.recent_savings.len() + 1;
116 metrics.average_savings_percent = total_percent / count as f64;
117
118 let command_type = savings
120 .command
121 .split_whitespace()
122 .next()
123 .unwrap_or("unknown")
124 .to_string();
125
126 let entry = metrics
127 .savings_by_command
128 .entry(command_type)
129 .or_insert_with(|| CommandSavings {
130 execution_count: 0,
131 total_tokens_saved: 0,
132 average_savings_percent: 0.0,
133 });
134
135 entry.execution_count += 1;
136 entry.total_tokens_saved += savings.tokens_saved;
137
138 let cmd_total_percent: f64 = entry.average_savings_percent
140 * (entry.execution_count - 1) as f64
141 + savings.savings_percent;
142 entry.average_savings_percent = cmd_total_percent / entry.execution_count as f64;
143
144 metrics.recent_savings.push(savings);
146 if metrics.recent_savings.len() > 100 {
147 metrics.recent_savings.remove(0);
148 }
149 }
150
151 pub async fn get_metrics(&self) -> RtkMetrics {
153 self.metrics.lock().await.clone()
154 }
155
156 pub async fn total_tokens_saved(&self) -> usize {
158 self.metrics.lock().await.total_tokens_saved
159 }
160
161 pub async fn total_commands(&self) -> usize {
163 self.metrics.lock().await.total_commands
164 }
165
166 pub async fn average_savings_percent(&self) -> f64 {
168 self.metrics.lock().await.average_savings_percent
169 }
170
171 pub async fn format_report(&self) -> String {
173 let metrics = self.get_metrics().await;
174
175 let mut report = String::new();
176 report.push_str("═══ RTK Token Savings Report ═══\n\n");
177
178 report.push_str(&format!("Total Commands: {}\n", metrics.total_commands));
179 report.push_str(&format!(
180 "Total Tokens Saved: {}\n",
181 metrics.total_tokens_saved
182 ));
183 report.push_str(&format!(
184 "Average Savings: {:.1}%\n",
185 metrics.average_savings_percent
186 ));
187 report.push_str(&format!(
188 "Tracking Since: {}\n\n",
189 metrics.tracking_since.format("%Y-%m-%d %H:%M:%S UTC")
190 ));
191
192 report.push_str("Savings by Command Type:\n");
193 let mut sorted_commands: Vec<_> = metrics.savings_by_command.iter().collect();
194 sorted_commands.sort_by_key(|b| std::cmp::Reverse(b.1.total_tokens_saved));
195
196 for (cmd, savings) in sorted_commands.iter().take(10) {
197 report.push_str(&format!(
198 " {}: {} cmds, {} tokens saved, {:.1}% avg\n",
199 cmd,
200 savings.execution_count,
201 savings.total_tokens_saved,
202 savings.average_savings_percent
203 ));
204 }
205
206 report
207 }
208}
209
210impl Default for RtkTracker {
211 fn default() -> Self {
212 Self::new()
213 }
214}