redis_enterprise/
stats.rs

1//! Statistics and metrics collection for Redis Enterprise
2//!
3//! ## Overview
4//! - Query cluster, node, database, and shard statistics
5//! - Retrieve time-series metrics with configurable intervals
6//! - Access both current and historical performance data
7//!
8//! ## Return Types
9//!
10//! Stats methods return either typed responses (`StatsResponse`, `LastStatsResponse`)
11//! or raw `serde_json::Value` for endpoints with dynamic metric names as keys.
12//! The Value returns allow access to all metrics without compile-time knowledge
13//! of metric names.
14//!
15//! ## Examples
16//!
17//! ### Querying Database Stats
18//! ```no_run
19//! use redis_enterprise::{EnterpriseClient, StatsHandler};
20//! use redis_enterprise::stats::StatsQuery;
21//!
22//! # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
23//! let stats = StatsHandler::new(client);
24//!
25//! // Get last interval stats for a database
26//! let last_stats = stats.database_last(1).await?;
27//! println!("Database stats: {:?}", last_stats);
28//!
29//! // Query with specific interval (all metrics by default)
30//! let query = StatsQuery {
31//!     interval: Some("5min".to_string()),
32//!     stime: None,
33//!     etime: None,
34//!     metrics: None,  // None means all metrics
35//! };
36//! let historical = stats.database(1, Some(query)).await?;
37//! println!("5-minute intervals: {:?}", historical.intervals);
38//! # Ok(())
39//! # }
40//! ```
41//!
42//! ### Cluster-Wide Statistics
43//! ```no_run
44//! # use redis_enterprise::{EnterpriseClient, StatsHandler};
45//! # async fn example(client: EnterpriseClient) -> Result<(), Box<dyn std::error::Error>> {
46//! let stats = StatsHandler::new(client);
47//!
48//! // Get aggregated stats for all nodes
49//! let all_nodes = stats.nodes_last().await?;
50//! println!("Total stats across cluster: {:?}", all_nodes.stats);
51//!
52//! // Get aggregated database stats
53//! let all_dbs = stats.databases_last().await?;
54//! for resource_stats in &all_dbs.stats {
55//!     println!("Resource {}: {:?}", resource_stats.uid, resource_stats.intervals);
56//! }
57//! # Ok(())
58//! # }
59//! ```
60
61use crate::client::RestClient;
62use crate::error::Result;
63use serde::{Deserialize, Serialize};
64use serde_json::Value;
65
66/// Stats query parameters
67#[derive(Debug, Serialize)]
68pub struct StatsQuery {
69    /// Time interval for aggregation ("1min", "5min", "1hour", "1day")
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub interval: Option<String>,
72    /// Start time for the query (ISO 8601 format)
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub stime: Option<String>,
75    /// End time for the query (ISO 8601 format)
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub etime: Option<String>,
78    /// Comma-separated list of specific metrics to retrieve
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub metrics: Option<String>,
81}
82
83/// Generic stats response
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct StatsResponse {
86    /// Array of time intervals with their corresponding metrics
87    pub intervals: Vec<StatsInterval>,
88
89    #[serde(flatten)]
90    pub extra: Value,
91}
92
93/// Stats interval
94#[derive(Debug, Clone, Serialize, Deserialize)]
95pub struct StatsInterval {
96    /// Timestamp for this interval (ISO 8601 format)
97    pub time: String,
98    /// Metrics data for this time interval (dynamic field names)
99    pub metrics: Value,
100}
101
102/// Last stats response for single resource
103/// Response for last stats endpoint - the API returns metrics directly
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct LastStatsResponse {
106    /// Start time of the stats interval
107    pub stime: Option<String>,
108    /// End time of the stats interval
109    pub etime: Option<String>,
110    /// Interval duration (e.g., "5min", "1hour")
111    pub interval: Option<String>,
112    /// All metric values for the last interval (dynamic field names)
113    #[serde(flatten)]
114    pub metrics: Value,
115}
116
117/// Aggregated stats response for multiple resources
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct AggregatedStatsResponse {
120    /// Array of stats for individual resources (nodes, databases, shards)
121    pub stats: Vec<ResourceStats>,
122    #[serde(flatten)]
123    pub extra: Value,
124}
125
126/// Stats for a single resource
127#[derive(Debug, Clone, Serialize, Deserialize)]
128pub struct ResourceStats {
129    /// Unique identifier of the resource (node UID, database UID, etc.)
130    pub uid: u32,
131    /// Time intervals with metrics for this specific resource
132    pub intervals: Vec<StatsInterval>,
133    #[serde(flatten)]
134    pub extra: Value,
135}
136
137/// Stats handler for retrieving metrics
138pub struct StatsHandler {
139    client: RestClient,
140}
141
142impl StatsHandler {
143    pub fn new(client: RestClient) -> Self {
144        StatsHandler { client }
145    }
146
147    /// Get cluster stats
148    pub async fn cluster(&self, query: Option<StatsQuery>) -> Result<StatsResponse> {
149        if let Some(q) = query {
150            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
151            self.client
152                .get(&format!("/v1/cluster/stats?{}", query_str))
153                .await
154        } else {
155            self.client.get("/v1/cluster/stats").await
156        }
157    }
158
159    /// Get cluster stats for last interval
160    pub async fn cluster_last(&self) -> Result<LastStatsResponse> {
161        self.client.get("/v1/cluster/stats/last").await
162    }
163
164    // raw variant removed: use cluster_last()
165
166    /// Get node stats
167    pub async fn node(&self, uid: u32, query: Option<StatsQuery>) -> Result<StatsResponse> {
168        if let Some(q) = query {
169            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
170            self.client
171                .get(&format!("/v1/nodes/{}/stats?{}", uid, query_str))
172                .await
173        } else {
174            self.client.get(&format!("/v1/nodes/{}/stats", uid)).await
175        }
176    }
177
178    /// Get node stats for last interval
179    pub async fn node_last(&self, uid: u32) -> Result<LastStatsResponse> {
180        self.client
181            .get(&format!("/v1/nodes/{}/stats/last", uid))
182            .await
183    }
184
185    // raw variant removed: use node_last()
186
187    /// Get all nodes stats
188    pub async fn nodes(&self, query: Option<StatsQuery>) -> Result<AggregatedStatsResponse> {
189        if let Some(q) = query {
190            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
191            self.client
192                .get(&format!("/v1/nodes/stats?{}", query_str))
193                .await
194        } else {
195            self.client.get("/v1/nodes/stats").await
196        }
197    }
198
199    // raw variant removed: use nodes()
200
201    /// Get all nodes last stats
202    pub async fn nodes_last(&self) -> Result<AggregatedStatsResponse> {
203        self.client.get("/v1/nodes/stats/last").await
204    }
205
206    // raw variant removed: use nodes_last()
207
208    /// Get node stats via alternate path form
209    pub async fn node_alt(&self, uid: u32) -> Result<StatsResponse> {
210        self.client.get(&format!("/v1/nodes/stats/{}", uid)).await
211    }
212
213    /// Get node last stats via alternate path form
214    pub async fn node_last_alt(&self, uid: u32) -> Result<LastStatsResponse> {
215        self.client
216            .get(&format!("/v1/nodes/stats/last/{}", uid))
217            .await
218    }
219
220    /// Get database stats
221    pub async fn database(&self, uid: u32, query: Option<StatsQuery>) -> Result<StatsResponse> {
222        if let Some(q) = query {
223            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
224            self.client
225                .get(&format!("/v1/bdbs/{}/stats?{}", uid, query_str))
226                .await
227        } else {
228            self.client.get(&format!("/v1/bdbs/{}/stats", uid)).await
229        }
230    }
231
232    /// Get database stats for last interval
233    pub async fn database_last(&self, uid: u32) -> Result<LastStatsResponse> {
234        self.client
235            .get(&format!("/v1/bdbs/{}/stats/last", uid))
236            .await
237    }
238
239    // raw variant removed: use database_last()
240
241    /// Get all databases stats
242    pub async fn databases(&self, query: Option<StatsQuery>) -> Result<AggregatedStatsResponse> {
243        if let Some(q) = query {
244            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
245            self.client
246                .get(&format!("/v1/bdbs/stats?{}", query_str))
247                .await
248        } else {
249            self.client.get("/v1/bdbs/stats").await
250        }
251    }
252
253    // raw variant removed: use databases()
254
255    /// Get all databases last stats (aggregate)
256    pub async fn databases_last(&self) -> Result<AggregatedStatsResponse> {
257        self.client.get("/v1/bdbs/stats/last").await
258    }
259
260    // raw variant removed: use databases_last()
261
262    /// Get database stats via alternate path form
263    pub async fn database_alt(&self, uid: u32) -> Result<StatsResponse> {
264        self.client.get(&format!("/v1/bdbs/stats/{}", uid)).await
265    }
266
267    /// Get database last stats via alternate path form
268    pub async fn database_last_alt(&self, uid: u32) -> Result<LastStatsResponse> {
269        self.client
270            .get(&format!("/v1/bdbs/stats/last/{}", uid))
271            .await
272    }
273
274    /// Get shard stats
275    pub async fn shard(&self, uid: u32, query: Option<StatsQuery>) -> Result<StatsResponse> {
276        if let Some(q) = query {
277            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
278            self.client
279                .get(&format!("/v1/shards/{}/stats?{}", uid, query_str))
280                .await
281        } else {
282            self.client.get(&format!("/v1/shards/{}/stats", uid)).await
283        }
284    }
285
286    /// Get all shards stats
287    pub async fn shards(&self, query: Option<StatsQuery>) -> Result<AggregatedStatsResponse> {
288        if let Some(q) = query {
289            let query_str = serde_urlencoded::to_string(&q).unwrap_or_default();
290            self.client
291                .get(&format!("/v1/shards/stats?{}", query_str))
292                .await
293        } else {
294            self.client.get("/v1/shards/stats").await
295        }
296    }
297
298    // raw variant removed: use shards()
299}