1use anyhow::Result;
10use chrono::{DateTime, Utc};
11use clap::Args;
12use schemars::JsonSchema;
13use serde::{Deserialize, Serialize};
14use std::path::PathBuf;
15use systemprompt_analytics::OverviewAnalyticsRepository;
16use systemprompt_logging::CliService;
17use systemprompt_runtime::{AppContext, DatabaseContext};
18
19use super::shared::{CsvBuilder, parse_time_range, resolve_export_path};
20use crate::CliConfig;
21use crate::shared::CommandOutput;
22
23#[derive(Debug, Args)]
24pub struct OverviewArgs {
25 #[arg(
26 long,
27 alias = "from",
28 default_value = "24h",
29 help = "Time range (e.g., '1h', '24h', '7d')"
30 )]
31 pub since: Option<String>,
32
33 #[arg(long, alias = "to", help = "End time for range")]
34 pub until: Option<String>,
35
36 #[arg(long, help = "Export results to CSV file")]
37 pub export: Option<PathBuf>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
41pub struct OverviewOutput {
42 pub period: String,
43 pub conversations: ConversationMetrics,
44 pub agents: AgentMetrics,
45 pub requests: RequestMetrics,
46 pub tools: ToolMetrics,
47 pub sessions: SessionMetrics,
48 pub costs: CostMetrics,
49}
50
51#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
52pub struct ConversationMetrics {
53 pub total: i64,
54 pub change_percent: Option<f64>,
55}
56
57#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
58pub struct AgentMetrics {
59 pub active_count: i64,
60 pub total_tasks: i64,
61 pub success_rate: f64,
62}
63
64#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
65pub struct RequestMetrics {
66 pub total: i64,
67 pub total_tokens: i64,
68 pub avg_latency_ms: i64,
69}
70
71#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
72pub struct ToolMetrics {
73 pub total_executions: i64,
74 pub success_rate: f64,
75}
76
77#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
78pub struct SessionMetrics {
79 #[serde(rename = "currently_active")]
80 pub active: i64,
81 #[serde(rename = "created_in_period")]
82 pub total_today: i64,
83}
84
85#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
86pub struct CostMetrics {
87 pub total_cost_microdollars: i64,
88 pub change_percent: Option<f64>,
89}
90
91pub async fn execute(args: OverviewArgs, _config: &CliConfig) -> Result<CommandOutput> {
92 let ctx = AppContext::new().await?;
93 let repo = OverviewAnalyticsRepository::new(ctx.db_pool())?;
94 execute_internal(args, &repo).await
95}
96
97pub async fn execute_with_pool(
98 args: OverviewArgs,
99 db_ctx: &DatabaseContext,
100 _config: &CliConfig,
101) -> Result<CommandOutput> {
102 let repo = OverviewAnalyticsRepository::new(db_ctx.db_pool())?;
103 execute_internal(args, &repo).await
104}
105
106async fn execute_internal(
107 args: OverviewArgs,
108 repo: &OverviewAnalyticsRepository,
109) -> Result<CommandOutput> {
110 let (start, end) = parse_time_range(args.since.as_ref(), args.until.as_ref())?;
111 let output = fetch_overview_data(repo, start, end).await?;
112
113 if let Some(ref path) = args.export {
114 let resolved_path = resolve_export_path(path)?;
115 export_overview_csv(&output, &resolved_path)?;
116 CliService::success(&format!("Exported to {}", resolved_path.display()));
117 return Ok(CommandOutput::card_value("Analytics Overview", &output).with_skip_render());
118 }
119
120 Ok(CommandOutput::card_value("Analytics Overview", &output))
121}
122
123async fn fetch_overview_data(
124 repo: &OverviewAnalyticsRepository,
125 start: DateTime<Utc>,
126 end: DateTime<Utc>,
127) -> Result<OverviewOutput> {
128 let period_duration = end - start;
129 let prev_start = start - period_duration;
130
131 let current_conversations = repo.get_conversation_count(start, end).await?;
132 let prev_conversations = repo.get_conversation_count(prev_start, start).await?;
133
134 let conversations = ConversationMetrics {
135 total: current_conversations,
136 change_percent: calculate_change(current_conversations, prev_conversations),
137 };
138
139 let agent_metrics = repo.get_agent_metrics(start, end).await?;
140 let success_rate = if agent_metrics.total_tasks > 0 {
141 (agent_metrics.completed_tasks as f64 / agent_metrics.total_tasks as f64) * 100.0
142 } else {
143 0.0
144 };
145
146 let agents = AgentMetrics {
147 active_count: agent_metrics.active_agents,
148 total_tasks: agent_metrics.total_tasks,
149 success_rate,
150 };
151
152 let request_metrics = repo.get_request_metrics(start, end).await?;
153 let requests = RequestMetrics {
154 total: request_metrics.total,
155 total_tokens: request_metrics.total_tokens.unwrap_or(0),
156 avg_latency_ms: request_metrics.avg_latency.map_or(0, |v| v as i64),
157 };
158
159 let tool_metrics = repo.get_tool_metrics(start, end).await?;
160 let tool_success_rate = if tool_metrics.total > 0 {
161 (tool_metrics.successful as f64 / tool_metrics.total as f64) * 100.0
162 } else {
163 0.0
164 };
165
166 let tools = ToolMetrics {
167 total_executions: tool_metrics.total,
168 success_rate: tool_success_rate,
169 };
170
171 let active_sessions = repo.get_active_session_count(start).await?;
172 let total_sessions = repo.get_total_session_count(start, end).await?;
173
174 let sessions = SessionMetrics {
175 active: active_sessions,
176 total_today: total_sessions,
177 };
178
179 let current_cost = repo.get_cost(start, end).await?;
180 let prev_cost = repo.get_cost(prev_start, start).await?;
181
182 let costs = CostMetrics {
183 total_cost_microdollars: current_cost.cost.unwrap_or(0),
184 change_percent: calculate_change(
185 current_cost.cost.unwrap_or(0),
186 prev_cost.cost.unwrap_or(0),
187 ),
188 };
189
190 Ok(OverviewOutput {
191 period: format_period(start, end),
192 conversations,
193 agents,
194 requests,
195 tools,
196 sessions,
197 costs,
198 })
199}
200
201fn calculate_change(current: i64, previous: i64) -> Option<f64> {
202 (previous != 0).then(|| ((current - previous) as f64 / previous as f64) * 100.0)
203}
204
205fn format_period(start: DateTime<Utc>, end: DateTime<Utc>) -> String {
206 format!(
207 "{} to {}",
208 start.format("%Y-%m-%d %H:%M"),
209 end.format("%Y-%m-%d %H:%M")
210 )
211}
212
213fn export_overview_csv(output: &OverviewOutput, path: &std::path::Path) -> Result<()> {
214 let mut csv = CsvBuilder::new().headers(vec![
215 "period",
216 "conversations_total",
217 "conversations_change_pct",
218 "agents_active",
219 "agents_tasks",
220 "agents_success_rate",
221 "requests_total",
222 "requests_tokens",
223 "requests_avg_latency_ms",
224 "tools_executions",
225 "tools_success_rate",
226 "sessions_currently_active",
227 "sessions_created_in_period",
228 "costs_microdollars",
229 "costs_change_pct",
230 ]);
231
232 csv.add_row(vec![
233 output.period.clone(),
234 output.conversations.total.to_string(),
235 output
236 .conversations
237 .change_percent
238 .map_or(String::new(), |v| format!("{:.2}", v)),
239 output.agents.active_count.to_string(),
240 output.agents.total_tasks.to_string(),
241 format!("{:.2}", output.agents.success_rate),
242 output.requests.total.to_string(),
243 output.requests.total_tokens.to_string(),
244 output.requests.avg_latency_ms.to_string(),
245 output.tools.total_executions.to_string(),
246 format!("{:.2}", output.tools.success_rate),
247 output.sessions.active.to_string(),
248 output.sessions.total_today.to_string(),
249 output.costs.total_cost_microdollars.to_string(),
250 output
251 .costs
252 .change_percent
253 .map_or(String::new(), |v| format!("{:.2}", v)),
254 ]);
255
256 csv.write_to_file(path)
257}