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::{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}