Skip to main content

unifly_api/legacy/
stats.rs

1// Legacy API statistics endpoints
2//
3// Historical reports (stat/report/) and DPI statistics (stat/sitedpi).
4// These endpoints return loosely-typed JSON because the field set varies
5// by report type, interval, and firmware version.
6
7use serde_json::json;
8use tracing::debug;
9
10use crate::error::Error;
11use crate::legacy::client::LegacyClient;
12
13fn attrs_or_default(attrs: Option<&[String]>, default: &[&str]) -> serde_json::Value {
14    attrs.map_or_else(|| json!(default), |custom| json!(custom))
15}
16
17impl LegacyClient {
18    /// Fetch site-level historical statistics.
19    ///
20    /// `POST /api/s/{site}/stat/report/{interval}.site`
21    ///
22    /// The `interval` parameter should be one of: `"5minutes"`, `"hourly"`, `"daily"`.
23    /// Returns loosely-typed JSON because the field set varies by report type.
24    pub async fn get_site_stats(
25        &self,
26        interval: &str,
27        start: Option<i64>,
28        end: Option<i64>,
29        attrs: Option<&[String]>,
30    ) -> Result<Vec<serde_json::Value>, Error> {
31        let path = format!("stat/report/{interval}.site");
32        let url = self.site_url(&path);
33        debug!(interval, ?start, ?end, "fetching site stats");
34
35        // The report endpoint requires a POST with attribute selection.
36        // Requesting common attributes; the API ignores unknown ones.
37        let mut body = json!({
38            "attrs": attrs_or_default(
39                attrs,
40                &["bytes", "num_sta", "time", "wlan-num_sta", "lan-num_sta"],
41            ),
42        });
43        if let Some(s) = start {
44            body["start"] = json!(s);
45        }
46        if let Some(e) = end {
47            body["end"] = json!(e);
48        }
49
50        self.post(url, &body).await
51    }
52
53    /// Fetch per-device historical statistics.
54    ///
55    /// `POST /api/s/{site}/stat/report/{interval}.device`
56    ///
57    /// If `macs` is provided, results are filtered to those devices.
58    pub async fn get_device_stats(
59        &self,
60        interval: &str,
61        macs: Option<&[String]>,
62        attrs: Option<&[String]>,
63    ) -> Result<Vec<serde_json::Value>, Error> {
64        let path = format!("stat/report/{interval}.device");
65        let url = self.site_url(&path);
66        debug!(interval, "fetching device stats");
67
68        let mut body = json!({
69            "attrs": attrs_or_default(attrs, &["bytes", "num_sta", "time", "rx_bytes", "tx_bytes"]),
70        });
71        if let Some(m) = macs {
72            body["macs"] = json!(m);
73        }
74
75        self.post(url, &body).await
76    }
77
78    /// Fetch per-client historical statistics.
79    ///
80    /// `POST /api/s/{site}/stat/report/{interval}.user`
81    ///
82    /// If `macs` is provided, results are filtered to those clients.
83    pub async fn get_client_stats(
84        &self,
85        interval: &str,
86        macs: Option<&[String]>,
87        attrs: Option<&[String]>,
88    ) -> Result<Vec<serde_json::Value>, Error> {
89        let path = format!("stat/report/{interval}.user");
90        let url = self.site_url(&path);
91        debug!(interval, "fetching client stats");
92
93        let mut body = json!({
94            "attrs": attrs_or_default(attrs, &["bytes", "time", "rx_bytes", "tx_bytes"]),
95        });
96        if let Some(m) = macs {
97            body["macs"] = json!(m);
98        }
99
100        self.post(url, &body).await
101    }
102
103    /// Fetch gateway historical statistics.
104    ///
105    /// `POST /api/s/{site}/stat/report/{interval}.gw`
106    pub async fn get_gateway_stats(
107        &self,
108        interval: &str,
109        start: Option<i64>,
110        end: Option<i64>,
111        attrs: Option<&[String]>,
112    ) -> Result<Vec<serde_json::Value>, Error> {
113        let path = format!("stat/report/{interval}.gw");
114        let url = self.site_url(&path);
115        debug!(interval, ?start, ?end, "fetching gateway stats");
116
117        let mut body = json!({
118            "attrs": attrs_or_default(
119                attrs,
120                &[
121                    "bytes",
122                    "time",
123                    "wan-tx_bytes",
124                    "wan-rx_bytes",
125                    "lan-rx_bytes",
126                    "lan-tx_bytes",
127                ],
128            ),
129        });
130        if let Some(s) = start {
131            body["start"] = json!(s);
132        }
133        if let Some(e) = end {
134            body["end"] = json!(e);
135        }
136
137        self.post(url, &body).await
138    }
139
140    /// Fetch site-wide DPI (Deep Packet Inspection) statistics.
141    ///
142    /// `POST /api/s/{site}/stat/sitedpi` with `{"type": "by_app"}` body.
143    ///
144    /// The `group_by` parameter selects the DPI grouping: `"by_app"` or `"by_cat"`.
145    /// Returns empty data if DPI tracking is not enabled on the controller.
146    pub async fn get_dpi_stats(
147        &self,
148        group_by: &str,
149        macs: Option<&[String]>,
150    ) -> Result<Vec<serde_json::Value>, Error> {
151        let url = self.site_url("stat/sitedpi");
152        debug!(group_by, "fetching site DPI stats");
153        let mut body = json!({"type": group_by});
154        if let Some(m) = macs {
155            body["macs"] = json!(m);
156        }
157        self.post(url, &body).await
158    }
159}