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 /// Filter by concurrency group: only runs that belong to this group.
79 pub concurrency_group: Option<String>,
80}
81
82impl StatsHistoryQuery {
83 /// Parse the comma-separated `label` param into a `HashMap`.
84 ///
85 /// # Examples
86 ///
87 /// ```
88 /// use ironflow_api::entities::StatsHistoryQuery;
89 ///
90 /// let query = StatsHistoryQuery {
91 /// workflow: None,
92 /// period: None,
93 /// granularity: None,
94 /// status: None,
95 /// has_steps: None,
96 /// label: Some("env:prod,team:core".to_string()),
97 /// created_by: None,
98 /// concurrency_group: None,
99 /// };
100 /// let labels = query.parse_labels().unwrap_or_default();
101 /// assert_eq!(labels.get("env").map(String::as_str), Some("prod"));
102 /// assert_eq!(labels.len(), 2);
103 /// ```
104 pub fn parse_labels(&self) -> Option<HashMap<String, String>> {
105 parse_label_param(&self.label)
106 }
107}
108
109/// Time-bucketed historical statistics response.
110///
111/// # Examples
112///
113/// ```
114/// use ironflow_api::entities::StatsHistoryResponse;
115/// ```
116#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
117#[derive(Debug, Serialize)]
118pub struct StatsHistoryResponse {
119 /// The period that was queried.
120 pub period: HistoryPeriod,
121 /// The granularity of each bucket.
122 pub granularity: HistoryGranularity,
123 /// Workflow name filter, if applied.
124 pub workflow: Option<String>,
125 /// Every bucket of the period, zero-filled, sorted by time ascending.
126 pub buckets: Vec<StatsHistoryBucketResponse>,
127}
128
129/// One time bucket in the history response.
130///
131/// Runs are assigned to the bucket of their creation time and counted under
132/// their current status. Each status has its own counter, so the counters add
133/// up to the number of runs created in the bucket.
134///
135/// # Examples
136///
137/// ```
138/// use ironflow_api::entities::StatsHistoryBucketResponse;
139/// ```
140#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
141#[derive(Debug, Serialize)]
142pub struct StatsHistoryBucketResponse {
143 /// Start of the time bucket.
144 pub time: DateTime<Utc>,
145 /// Number of runs created in this bucket and currently completed.
146 /// Strict: runs in the `warning` state are counted in `warning`.
147 pub completed: u64,
148 /// Number of runs created in this bucket and currently in `warning`.
149 pub warning: u64,
150 /// Number of runs created in this bucket and currently failed.
151 pub failed: u64,
152 /// Number of runs created in this bucket and currently cancelled.
153 pub cancelled: u64,
154 /// Number of runs created in this bucket and currently pending.
155 pub pending: u64,
156 /// Number of runs created in this bucket and currently running.
157 pub running: u64,
158 /// Number of runs created in this bucket and currently retrying.
159 pub retrying: u64,
160 /// Number of runs created in this bucket and currently awaiting approval.
161 pub awaiting_approval: u64,
162 /// Number of runs created in this bucket and currently sleeping.
163 pub sleeping: u64,
164 /// Number of runs created in this bucket and currently paused.
165 pub paused: u64,
166 /// Success rate: (completed + warning) / (completed + warning + failed),
167 /// as a percentage. `null` when the bucket has no completed, warning or
168 /// failed run.
169 pub success_rate_percent: Option<f64>,
170 /// Average duration in milliseconds.
171 pub avg_duration_ms: u64,
172 /// 95th percentile duration in milliseconds.
173 pub p95_duration_ms: u64,
174 /// Total cost in USD.
175 pub total_cost_usd: Decimal,
176}
177
178impl From<StatsHistoryBucket> for StatsHistoryBucketResponse {
179 fn from(b: StatsHistoryBucket) -> Self {
180 let success_rate_percent = b.success_rate_percent();
181 StatsHistoryBucketResponse {
182 time: b.time,
183 completed: b.completed,
184 warning: b.warning,
185 failed: b.failed,
186 cancelled: b.cancelled,
187 pending: b.pending,
188 running: b.running,
189 retrying: b.retrying,
190 awaiting_approval: b.awaiting_approval,
191 sleeping: b.sleeping,
192 paused: b.paused,
193 success_rate_percent,
194 avg_duration_ms: b.avg_duration_ms,
195 p95_duration_ms: b.p95_duration_ms,
196 total_cost_usd: b.total_cost_usd,
197 }
198 }
199}