ironflow_api/entities/stats.rs
1//! Statistics DTOs.
2
3use std::collections::HashMap;
4
5use chrono::{DateTime, Utc};
6use rust_decimal::Decimal;
7use serde::{Deserialize, Serialize};
8use uuid::Uuid;
9
10use ironflow_store::entities::{
11 AgentRoleStats, HistoryGranularity, HistoryPeriod, StatsHistoryBucket,
12};
13use ironflow_store::models::RunStatus;
14
15use super::run::parse_label_param;
16
17/// Aggregate statistics response.
18///
19/// Computed from all runs in the store.
20///
21/// # Examples
22///
23/// ```
24/// use ironflow_api::entities::StatsResponse;
25/// ```
26#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
27#[derive(Debug, Serialize)]
28pub struct StatsResponse {
29 /// Total number of runs.
30 pub total_runs: u64,
31 /// Number of completed runs.
32 pub completed_runs: u64,
33 /// Number of failed runs.
34 pub failed_runs: u64,
35 /// Number of cancelled runs.
36 pub cancelled_runs: u64,
37 /// Number of active runs: pending, running, retrying, awaiting approval
38 /// or sleeping.
39 pub active_runs: u64,
40 /// Number of runs awaiting approval. A subset of `active_runs`.
41 pub awaiting_approval_runs: u64,
42 /// Success rate: completed / (completed + failed), as a percentage.
43 pub success_rate_percent: f64,
44 /// Aggregated cost across all runs in USD.
45 pub total_cost_usd: Decimal,
46 /// Aggregated duration across all runs in milliseconds.
47 pub total_duration_ms: u64,
48}
49
50/// Query parameters for `GET /api/v1/stats/history`.
51///
52/// # Examples
53///
54/// ```
55/// use ironflow_api::entities::StatsHistoryQuery;
56/// ```
57#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
58#[derive(Debug, Deserialize)]
59pub struct StatsHistoryQuery {
60 /// Filter by workflow name (case-insensitive substring match, same as
61 /// `GET /api/v1/runs`). Omit to aggregate all workflows.
62 pub workflow: Option<String>,
63 /// Time period to query. Defaults to `7d`.
64 pub period: Option<HistoryPeriod>,
65 /// Bucket granularity. Auto-derived from period when omitted.
66 pub granularity: Option<HistoryGranularity>,
67 /// Filter by run status.
68 pub status: Option<RunStatus>,
69 /// Filter by step presence (only applies to completed/cancelled runs).
70 /// Non-terminal runs (pending, running, etc.) are always included.
71 /// When `true`, only count completed/cancelled runs that have steps.
72 /// When `false`, only count completed/cancelled runs without steps.
73 pub has_steps: Option<bool>,
74 /// Filter by labels. Comma-separated `key:value` pairs.
75 pub label: Option<String>,
76 /// Filter by author: the user ID that triggered the run.
77 ///
78 /// Also matches runs triggered by one of that user's API keys.
79 pub created_by: Option<Uuid>,
80 /// Filter by concurrency group: only runs that belong to this group.
81 pub concurrency_group: Option<String>,
82}
83
84impl StatsHistoryQuery {
85 /// Parse the comma-separated `label` param into a `HashMap`.
86 ///
87 /// # Examples
88 ///
89 /// ```
90 /// use ironflow_api::entities::StatsHistoryQuery;
91 ///
92 /// let query = StatsHistoryQuery {
93 /// workflow: None,
94 /// period: None,
95 /// granularity: None,
96 /// status: None,
97 /// has_steps: None,
98 /// label: Some("env:prod,team:core".to_string()),
99 /// created_by: None,
100 /// concurrency_group: None,
101 /// };
102 /// let labels = query.parse_labels().unwrap_or_default();
103 /// assert_eq!(labels.get("env").map(String::as_str), Some("prod"));
104 /// assert_eq!(labels.len(), 2);
105 /// ```
106 pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
107 parse_label_param(&self.label)
108 }
109}
110
111/// Time-bucketed historical statistics response.
112///
113/// # Examples
114///
115/// ```
116/// use ironflow_api::entities::StatsHistoryResponse;
117/// ```
118#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
119#[derive(Debug, Serialize)]
120pub struct StatsHistoryResponse {
121 /// The period that was queried.
122 pub period: HistoryPeriod,
123 /// The granularity of each bucket.
124 pub granularity: HistoryGranularity,
125 /// Workflow name filter, if applied.
126 pub workflow: Option<String>,
127 /// Every bucket of the period, zero-filled, sorted by time ascending.
128 pub buckets: Vec<StatsHistoryBucketResponse>,
129}
130
131/// One time bucket in the history response.
132///
133/// Runs are assigned to the bucket of their creation time and counted under
134/// their current status. Each status has its own counter, so the counters add
135/// up to the number of runs created in the bucket.
136///
137/// # Examples
138///
139/// ```
140/// use ironflow_api::entities::StatsHistoryBucketResponse;
141/// ```
142#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
143#[derive(Debug, Serialize)]
144pub struct StatsHistoryBucketResponse {
145 /// Start of the time bucket.
146 pub time: DateTime<Utc>,
147 /// Number of runs created in this bucket and currently completed.
148 /// Strict: runs in the `warning` state are counted in `warning`.
149 pub completed: u64,
150 /// Number of runs created in this bucket and currently in `warning`.
151 pub warning: u64,
152 /// Number of runs created in this bucket and currently failed.
153 pub failed: u64,
154 /// Number of runs created in this bucket and currently cancelled.
155 pub cancelled: u64,
156 /// Number of runs created in this bucket and currently pending.
157 pub pending: u64,
158 /// Number of runs created in this bucket and currently running.
159 pub running: u64,
160 /// Number of runs created in this bucket and currently retrying.
161 pub retrying: u64,
162 /// Number of runs created in this bucket and currently awaiting approval.
163 pub awaiting_approval: u64,
164 /// Number of runs created in this bucket and currently sleeping.
165 pub sleeping: u64,
166 /// Number of runs created in this bucket and currently paused.
167 pub paused: u64,
168 /// Success rate: (completed + warning) / (completed + warning + failed),
169 /// as a percentage. `null` when the bucket has no completed, warning or
170 /// failed run.
171 pub success_rate_percent: Option<f64>,
172 /// Average duration in milliseconds.
173 pub avg_duration_ms: u64,
174 /// 95th percentile duration in milliseconds.
175 pub p95_duration_ms: u64,
176 /// Total cost in USD.
177 pub total_cost_usd: Decimal,
178}
179
180impl From<StatsHistoryBucket> for StatsHistoryBucketResponse {
181 fn from(b: StatsHistoryBucket) -> Self {
182 let success_rate_percent = b.success_rate_percent();
183 StatsHistoryBucketResponse {
184 time: b.time,
185 completed: b.completed,
186 warning: b.warning,
187 failed: b.failed,
188 cancelled: b.cancelled,
189 pending: b.pending,
190 running: b.running,
191 retrying: b.retrying,
192 awaiting_approval: b.awaiting_approval,
193 sleeping: b.sleeping,
194 paused: b.paused,
195 success_rate_percent,
196 avg_duration_ms: b.avg_duration_ms,
197 p95_duration_ms: b.p95_duration_ms,
198 total_cost_usd: b.total_cost_usd,
199 }
200 }
201}
202
203/// Query parameters for `GET /api/v1/stats/agent-roles`.
204///
205/// # Examples
206///
207/// ```
208/// use ironflow_api::entities::AgentRoleStatsQuery;
209/// use ironflow_store::entities::HistoryPeriod;
210///
211/// let query = AgentRoleStatsQuery {
212/// period: Some(HistoryPeriod::ThirtyDays),
213/// };
214/// assert_eq!(query.period, Some(HistoryPeriod::ThirtyDays));
215/// ```
216#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
217#[derive(Debug, Deserialize)]
218pub struct AgentRoleStatsQuery {
219 /// Time period to query: `24h`, `7d`, `30d` or `90d`. Defaults to `7d`.
220 pub period: Option<HistoryPeriod>,
221}
222
223/// Cost, executions and failures per agent role over a period, all workflows
224/// combined.
225///
226/// # Examples
227///
228/// ```
229/// use ironflow_api::entities::AgentRoleStatsResponse;
230/// use ironflow_store::entities::HistoryPeriod;
231///
232/// let response = AgentRoleStatsResponse {
233/// period: HistoryPeriod::SevenDays,
234/// roles: Vec::new(),
235/// };
236/// assert!(response.roles.is_empty());
237/// ```
238#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
239#[derive(Debug, Serialize)]
240pub struct AgentRoleStatsResponse {
241 /// The period that was queried.
242 pub period: HistoryPeriod,
243 /// One entry per role that ran an agent step in the period, sorted by
244 /// total cost descending, then by role name.
245 pub roles: Vec<AgentRoleStatsEntryResponse>,
246}
247
248/// Cost, executions and failures of one agent role.
249///
250/// # Examples
251///
252/// ```
253/// use ironflow_api::entities::AgentRoleStatsEntryResponse;
254/// use ironflow_store::entities::AgentRoleStats;
255/// use rust_decimal::Decimal;
256///
257/// let entry = AgentRoleStatsEntryResponse::from(AgentRoleStats {
258/// agent_role: "Reviewer".to_string(),
259/// executions: 3,
260/// failed: 1,
261/// total_cost_usd: Decimal::new(42, 2),
262/// total_duration_ms: 1200,
263/// });
264/// assert_eq!(entry.agent_role, "Reviewer");
265/// ```
266#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
267#[derive(Debug, Serialize)]
268pub struct AgentRoleStatsEntryResponse {
269 /// Name of the role (`AgentRole::name`).
270 pub agent_role: String,
271 /// Number of agent steps the role ran, each attempt included.
272 pub executions: u64,
273 /// Steps of the role that failed.
274 pub failed: u64,
275 /// Total cost in USD of the steps of the role.
276 pub total_cost_usd: Decimal,
277 /// Total execution time in milliseconds of the steps of the role.
278 pub total_duration_ms: u64,
279}
280
281impl From<AgentRoleStats> for AgentRoleStatsEntryResponse {
282 fn from(stats: AgentRoleStats) -> Self {
283 AgentRoleStatsEntryResponse {
284 agent_role: stats.agent_role,
285 executions: stats.executions,
286 failed: stats.failed,
287 total_cost_usd: stats.total_cost_usd,
288 total_duration_ms: stats.total_duration_ms,
289 }
290 }
291}