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::{HistoryGranularity, HistoryPeriod, StatsHistoryBucket};
11use ironflow_store::models::RunStatus;
12
13use super::run::parse_label_param;
14
15/// Aggregate statistics response.
16///
17/// Computed from all runs in the store.
18///
19/// # Examples
20///
21/// ```
22/// use ironflow_api::entities::StatsResponse;
23/// ```
24#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
25#[derive(Debug, Serialize)]
26pub struct StatsResponse {
27 /// Total number of runs.
28 pub total_runs: u64,
29 /// Number of completed runs.
30 pub completed_runs: u64,
31 /// Number of failed runs.
32 pub failed_runs: u64,
33 /// Number of cancelled runs.
34 pub cancelled_runs: u64,
35 /// Number of active runs: pending, running, retrying, awaiting approval
36 /// or sleeping.
37 pub active_runs: u64,
38 /// Number of runs awaiting approval. A subset of `active_runs`.
39 pub awaiting_approval_runs: u64,
40 /// Success rate: completed / (completed + failed), as a percentage.
41 pub success_rate_percent: f64,
42 /// Aggregated cost across all runs in USD.
43 pub total_cost_usd: Decimal,
44 /// Aggregated duration across all runs in milliseconds.
45 pub total_duration_ms: u64,
46}
47
48/// Query parameters for `GET /api/v1/stats/history`.
49///
50/// # Examples
51///
52/// ```
53/// use ironflow_api::entities::StatsHistoryQuery;
54/// ```
55#[cfg_attr(feature = "openapi", derive(utoipa::IntoParams))]
56#[derive(Debug, Deserialize)]
57pub struct StatsHistoryQuery {
58 /// Filter by workflow name (case-insensitive substring match, same as
59 /// `GET /api/v1/runs`). Omit to aggregate all workflows.
60 pub workflow: Option<String>,
61 /// Time period to query. Defaults to `7d`.
62 pub period: Option<HistoryPeriod>,
63 /// Bucket granularity. Auto-derived from period when omitted.
64 pub granularity: Option<HistoryGranularity>,
65 /// Filter by run status.
66 pub status: Option<RunStatus>,
67 /// Filter by step presence (only applies to completed/cancelled runs).
68 /// Non-terminal runs (pending, running, etc.) are always included.
69 /// When `true`, only count completed/cancelled runs that have steps.
70 /// When `false`, only count completed/cancelled runs without steps.
71 pub has_steps: Option<bool>,
72 /// Filter by labels. Comma-separated `key:value` pairs.
73 pub label: Option<String>,
74 /// Filter by author: the user ID that triggered the run.
75 ///
76 /// Also matches runs triggered by one of that user's API keys.
77 pub created_by: Option<Uuid>,
78}
79
80impl StatsHistoryQuery {
81 /// Parse the comma-separated `label` param into a `HashMap`.
82 ///
83 /// # Examples
84 ///
85 /// ```
86 /// use ironflow_api::entities::StatsHistoryQuery;
87 ///
88 /// let query = StatsHistoryQuery {
89 /// workflow: None,
90 /// period: None,
91 /// granularity: None,
92 /// status: None,
93 /// has_steps: None,
94 /// label: Some("env:prod,team:core".to_string()),
95 /// created_by: None,
96 /// };
97 /// let labels = query.parse_labels().unwrap_or_default();
98 /// assert_eq!(labels.get("env").map(String::as_str), Some("prod"));
99 /// assert_eq!(labels.len(), 2);
100 /// ```
101 pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
102 parse_label_param(&self.label)
103 }
104}
105
106/// Time-bucketed historical statistics response.
107///
108/// # Examples
109///
110/// ```
111/// use ironflow_api::entities::StatsHistoryResponse;
112/// ```
113#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
114#[derive(Debug, Serialize)]
115pub struct StatsHistoryResponse {
116 /// The period that was queried.
117 pub period: HistoryPeriod,
118 /// The granularity of each bucket.
119 pub granularity: HistoryGranularity,
120 /// Workflow name filter, if applied.
121 pub workflow: Option<String>,
122 /// Every bucket of the period, zero-filled, sorted by time ascending.
123 pub buckets: Vec<StatsHistoryBucketResponse>,
124}
125
126/// One time bucket in the history response.
127///
128/// Runs are assigned to the bucket of their creation time and counted under
129/// their current status. Each status has its own counter, so the counters add
130/// up to the number of runs created in the bucket.
131///
132/// # Examples
133///
134/// ```
135/// use ironflow_api::entities::StatsHistoryBucketResponse;
136/// ```
137#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
138#[derive(Debug, Serialize)]
139pub struct StatsHistoryBucketResponse {
140 /// Start of the time bucket.
141 pub time: DateTime<Utc>,
142 /// Number of runs created in this bucket and currently completed.
143 /// Strict: runs in the `warning` state are counted in `warning`.
144 pub completed: u64,
145 /// Number of runs created in this bucket and currently in `warning`.
146 pub warning: u64,
147 /// Number of runs created in this bucket and currently failed.
148 pub failed: u64,
149 /// Number of runs created in this bucket and currently cancelled.
150 pub cancelled: u64,
151 /// Number of runs created in this bucket and currently pending.
152 pub pending: u64,
153 /// Number of runs created in this bucket and currently running.
154 pub running: u64,
155 /// Number of runs created in this bucket and currently retrying.
156 pub retrying: u64,
157 /// Number of runs created in this bucket and currently awaiting approval.
158 pub awaiting_approval: u64,
159 /// Number of runs created in this bucket and currently sleeping.
160 pub sleeping: u64,
161 /// Success rate: (completed + warning) / (completed + warning + failed),
162 /// as a percentage. `null` when the bucket has no completed, warning or
163 /// failed run.
164 pub success_rate_percent: Option<f64>,
165 /// Average duration in milliseconds.
166 pub avg_duration_ms: u64,
167 /// 95th percentile duration in milliseconds.
168 pub p95_duration_ms: u64,
169 /// Total cost in USD.
170 pub total_cost_usd: Decimal,
171}
172
173impl From<StatsHistoryBucket> for StatsHistoryBucketResponse {
174 fn from(b: StatsHistoryBucket) -> Self {
175 let success_rate_percent = b.success_rate_percent();
176 StatsHistoryBucketResponse {
177 time: b.time,
178 completed: b.completed,
179 warning: b.warning,
180 failed: b.failed,
181 cancelled: b.cancelled,
182 pending: b.pending,
183 running: b.running,
184 retrying: b.retrying,
185 awaiting_approval: b.awaiting_approval,
186 sleeping: b.sleeping,
187 success_rate_percent,
188 avg_duration_ms: b.avg_duration_ms,
189 p95_duration_ms: b.p95_duration_ms,
190 total_cost_usd: b.total_cost_usd,
191 }
192 }
193}