Skip to main content

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}