haproxy_stats/
stat.rs

1use core::{fmt, ops::Deref};
2
3use csv::{Error as CsvError, ReaderBuilder};
4use serde::Deserialize;
5use serde_enum_str::Deserialize_enum_str;
6use serde_json::{Error as SerdeJsonError, Map, Value};
7
8use crate::formats::json;
9
10//
11pub const SVNAME_FRONTEND: &str = "FRONTEND";
12pub const SVNAME_BACKEND: &str = "BACKEND";
13
14//
15#[derive(Deserialize, Debug, Clone)]
16#[serde(tag = "type")]
17pub enum Statistic {
18    #[serde(rename = "0")]
19    Frontend(FrontendStatistic),
20    #[serde(rename = "1")]
21    Backend(BackendStatistic),
22    #[serde(rename = "2")]
23    Server(ServerStatistic),
24    #[serde(rename = "3")]
25    Listener(ListenerStatistic),
26}
27
28impl Statistic {
29    pub fn as_frontend(&self) -> Option<&FrontendStatistic> {
30        match self {
31            Self::Frontend(s) => Some(s),
32            _ => None,
33        }
34    }
35
36    pub fn as_backend(&self) -> Option<&BackendStatistic> {
37        match self {
38            Self::Backend(s) => Some(s),
39            _ => None,
40        }
41    }
42
43    pub fn as_server(&self) -> Option<&ServerStatistic> {
44        match self {
45            Self::Server(s) => Some(s),
46            _ => None,
47        }
48    }
49
50    pub fn as_listener(&self) -> Option<&ListenerStatistic> {
51        match self {
52            Self::Listener(s) => Some(s),
53            _ => None,
54        }
55    }
56}
57
58//
59#[derive(Deserialize, Debug, Clone)]
60pub struct ListenerStatistic {
61    pub pxname: Box<str>,
62    pub svname: Box<str>,
63    //
64    pub scur: usize,
65    pub smax: usize,
66    pub slim: usize,
67    pub stot: usize,
68    //
69    pub bin: usize,
70    pub bout: usize,
71    //
72    pub dreq: usize,
73    pub dresp: usize,
74    //
75    pub ereq: usize,
76    //
77    pub status: Status,
78    //
79    pub pid: usize,
80    pub iid: usize,
81    pub sid: usize,
82    // TODO,
83}
84
85//
86#[derive(Deserialize, Debug, Clone)]
87pub struct FrontendStatistic {
88    pub pxname: Box<str>,
89    // svname SKIP
90    //
91    pub scur: usize,
92    pub smax: usize,
93    pub slim: usize,
94    pub stot: usize,
95    //
96    pub bin: usize,
97    pub bout: usize,
98    //
99    pub dreq: usize,
100    pub dresp: usize,
101    //
102    pub ereq: usize,
103    //
104    pub status: Status,
105    //
106    pub pid: usize,
107    pub iid: usize,
108    //
109    pub rate: usize,
110    pub rate_lim: usize,
111    pub rate_max: usize,
112    //
113    pub hrsp_1xx: Option<usize>,
114    pub hrsp_2xx: Option<usize>,
115    pub hrsp_3xx: Option<usize>,
116    pub hrsp_4xx: Option<usize>,
117    pub hrsp_5xx: Option<usize>,
118    pub hrsp_other: Option<usize>,
119    //
120    pub req_rate: Option<usize>,
121    pub req_rate_max: Option<usize>,
122    pub req_tot: Option<usize>,
123    // TODO,
124}
125
126//
127#[derive(Deserialize, Debug, Clone)]
128pub struct BackendStatistic {
129    pub pxname: Box<str>,
130    // svname SKIP
131    //
132    pub qcur: usize,
133    pub qmax: usize,
134    //
135    pub scur: usize,
136    pub smax: usize,
137    pub slim: usize,
138    pub stot: usize,
139    //
140    pub bin: usize,
141    pub bout: usize,
142    //
143    pub dreq: usize,
144    pub dresp: usize,
145    //
146    pub econ: usize,
147    pub eresp: usize,
148    //
149    pub wretr: usize,
150    pub wredis: usize,
151    //
152    pub status: Status,
153    //
154    pub weight: usize,
155    //
156    pub act: usize,
157    pub bck: usize,
158    //
159    pub chkdown: usize,
160    pub lastchg: usize,
161    // Because missing maybe in 1.7.9
162    pub downtime: Option<usize>,
163    //
164    pub pid: usize,
165    pub iid: usize,
166    //
167    pub lbtot: usize,
168    //
169    pub rate: usize,
170    pub rate_max: usize,
171    //
172    pub hrsp_1xx: Option<usize>,
173    pub hrsp_2xx: Option<usize>,
174    pub hrsp_3xx: Option<usize>,
175    pub hrsp_4xx: Option<usize>,
176    pub hrsp_5xx: Option<usize>,
177    pub hrsp_other: Option<usize>,
178    //
179    pub req_tot: Option<usize>,
180    //
181    pub cli_abrt: usize,
182    pub srv_abrt: usize,
183    // TODO,
184}
185
186//
187#[derive(Deserialize, Debug, Clone)]
188pub struct ServerStatistic {
189    pub pxname: Box<str>,
190    pub svname: Box<str>,
191    //
192    pub qcur: usize,
193    pub qmax: usize,
194    //
195    pub scur: usize,
196    pub smax: usize,
197    pub slim: Option<usize>,
198    pub stot: usize,
199    //
200    pub bin: usize,
201    pub bout: usize,
202    //
203    pub dresp: usize,
204    //
205    pub econ: usize,
206    pub eresp: usize,
207    //
208    pub wretr: usize,
209    pub wredis: usize,
210    //
211    pub status: Status,
212    //
213    pub weight: usize,
214    //
215    pub act: usize,
216    pub bck: usize,
217    //
218    pub chkfail: Option<usize>,
219    pub chkdown: Option<usize>,
220    pub lastchg: Option<usize>,
221    pub downtime: Option<usize>,
222    //
223    pub qlimit: Option<usize>,
224    //
225    pub pid: usize,
226    pub iid: usize,
227    pub sid: usize,
228    //
229    pub throttle: Option<usize>,
230    //
231    pub lbtot: usize,
232    //
233    pub tracked: Option<usize>,
234    //
235    pub rate: usize,
236    pub rate_max: usize,
237    //
238    pub check_status: Option<CheckStatus>,
239    //
240    pub check_code: Option<usize>,
241    pub check_duration: Option<usize>,
242    //
243    pub hrsp_1xx: Option<usize>,
244    pub hrsp_2xx: Option<usize>,
245    pub hrsp_3xx: Option<usize>,
246    pub hrsp_4xx: Option<usize>,
247    pub hrsp_5xx: Option<usize>,
248    pub hrsp_other: Option<usize>,
249    //
250    // Because missing in json format
251    #[serde(default)]
252    pub hanafail: Box<str>,
253    //
254    pub cli_abrt: usize,
255    pub srv_abrt: usize,
256    //
257    // TODO,
258}
259
260//
261//
262//
263#[derive(Deserialize_enum_str, Debug, Clone, PartialEq)]
264#[serde(rename_all = "snake_case")]
265pub enum Status {
266    #[serde(rename = "UP")]
267    UP,
268    #[serde(rename = "DOWN")]
269    DOWN,
270    #[serde(rename = "OPEN")]
271    OPEN,
272    #[serde(other)]
273    Other(String),
274}
275
276#[derive(Deserialize_enum_str, Debug, Clone, PartialEq)]
277#[serde(rename_all = "snake_case")]
278pub enum CheckStatus {
279    #[serde(rename = "L4TOUT")]
280    L4TOUT,
281    #[serde(rename = "* L4TOUT")]
282    LastL4TOUT,
283    #[serde(rename = "L4CON")]
284    L4CON,
285    #[serde(rename = "* L4CON")]
286    LastL4CON,
287    #[serde(rename = "L7OK")]
288    L7OK,
289    #[serde(rename = "* L7OK")]
290    LastL7OK,
291    #[serde(other)]
292    Other(String),
293}
294
295//
296//
297//
298#[derive(Debug, Clone)]
299pub struct Statistics(pub Vec<Statistic>);
300
301impl Deref for Statistics {
302    type Target = Vec<Statistic>;
303
304    fn deref(&self) -> &Self::Target {
305        &self.0
306    }
307}
308
309impl Statistics {
310    pub fn from_csv_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, StatisticsFromCsvBytesError> {
311        let bytes = bytes.as_ref();
312        if &bytes[..1] != b"#" {
313            return Err(StatisticsFromCsvBytesError::Other(
314                "The first line begins with a sharp ('#')",
315            ));
316        }
317
318        let mut rdr = ReaderBuilder::new()
319            .has_headers(false)
320            .from_reader(&bytes[1..]);
321        let mut iter = rdr.records();
322
323        //
324        let (header, header_names) = if let Some(record) = iter.next() {
325            let mut record = record.map_err(StatisticsFromCsvBytesError::CsvParseFailed)?;
326            record.trim();
327            let list: Vec<Box<str>> = record
328                .deserialize(None)
329                .map_err(StatisticsFromCsvBytesError::HeaderDeFailed)?;
330            (record, list)
331        } else {
332            return Err(StatisticsFromCsvBytesError::HeaderMissing);
333        };
334
335        let header_type_position = header_names
336            .iter()
337            .position(|x| *x == "type".into())
338            .ok_or(StatisticsFromCsvBytesError::HeaderNameMismatch(
339                "type missing",
340            ))?;
341        let header_svname_position = header_names
342            .iter()
343            .position(|x| *x == "svname".into())
344            .ok_or(StatisticsFromCsvBytesError::HeaderNameMismatch(
345                "svname missing",
346            ))?;
347
348        let mut inner = vec![];
349        for (i, record) in iter.enumerate() {
350            let record = record.map_err(StatisticsFromCsvBytesError::CsvParseFailed)?;
351
352            let r#type = record.get(header_type_position).ok_or_else(|| {
353                StatisticsFromCsvBytesError::RowValueMismatch(
354                    format!(
355                        "line:{} position:{} type missing",
356                        i + 1,
357                        header_type_position
358                    )
359                    .into(),
360                )
361            })?;
362            let svname = record.get(header_svname_position).ok_or_else(|| {
363                StatisticsFromCsvBytesError::RowValueMismatch(
364                    format!(
365                        "line:{} position:{} svname missing",
366                        i + 1,
367                        header_svname_position
368                    )
369                    .into(),
370                )
371            })?;
372
373            match r#type {
374                "0" => {
375                    if svname != SVNAME_FRONTEND {
376                        return Err(StatisticsFromCsvBytesError::RowValueMismatch(
377                            format!(
378                                "line:{} svname:{} svname should eq {}",
379                                i + 1,
380                                svname,
381                                SVNAME_FRONTEND,
382                            )
383                            .into(),
384                        ));
385                    }
386
387                    let row: FrontendStatistic = record
388                        .deserialize(Some(&header))
389                        .map_err(StatisticsFromCsvBytesError::RowDeFailed)?;
390                    inner.push(Statistic::Frontend(row));
391                }
392                "1" => {
393                    if svname != SVNAME_BACKEND {
394                        return Err(StatisticsFromCsvBytesError::RowValueMismatch(
395                            format!(
396                                "line:{} svname:{} svname should eq {}",
397                                i + 1,
398                                svname,
399                                SVNAME_BACKEND,
400                            )
401                            .into(),
402                        ));
403                    }
404
405                    let row: BackendStatistic = record
406                        .deserialize(Some(&header))
407                        .map_err(StatisticsFromCsvBytesError::RowDeFailed)?;
408                    inner.push(Statistic::Backend(row));
409                }
410                "2" => {
411                    let row: ServerStatistic = record
412                        .deserialize(Some(&header))
413                        .map_err(StatisticsFromCsvBytesError::RowDeFailed)?;
414                    inner.push(Statistic::Server(row));
415                }
416                "4" => {
417                    let row: ListenerStatistic = record
418                        .deserialize(Some(&header))
419                        .map_err(StatisticsFromCsvBytesError::RowDeFailed)?;
420                    inner.push(Statistic::Listener(row));
421                }
422                _ => return Err(StatisticsFromCsvBytesError::UnknownType),
423            }
424        }
425
426        Ok(Self(inner))
427    }
428
429    pub fn from_json_bytes(bytes: impl AsRef<[u8]>) -> Result<Self, StatisticsFromJsonBytesError> {
430        let bytes = bytes.as_ref();
431
432        let output = serde_json::from_slice::<JsonOutput>(bytes)
433            .map_err(StatisticsFromJsonBytesError::DeOutputFailed)?;
434
435        let array: Vec<Value> = output
436            .0
437            .into_iter()
438            .map(|x| {
439                Value::Object(
440                    x.into_iter()
441                        .map(|y| {
442                            let v = match y.field.name.as_ref() {
443                                "type" => Value::from(y.value.value_to_string()),
444                                _ => Value::from(&y.value),
445                            };
446
447                            (y.field.name.to_string(), v)
448                        })
449                        .collect::<Map<String, Value>>(),
450                )
451            })
452            .collect();
453
454        let inner = serde_json::from_value::<Vec<Statistic>>(Value::Array(array))
455            .map_err(StatisticsFromJsonBytesError::DeFailed)?;
456
457        Ok(Self(inner))
458    }
459}
460
461//
462#[derive(Debug)]
463pub enum StatisticsFromCsvBytesError {
464    CsvParseFailed(CsvError),
465    HeaderMissing,
466    HeaderDeFailed(CsvError),
467    HeaderNameMismatch(&'static str),
468    RowDeFailed(CsvError),
469    RowValueMismatch(Box<str>),
470    UnknownType,
471    Other(&'static str),
472}
473
474impl fmt::Display for StatisticsFromCsvBytesError {
475    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
476        write!(f, "{:?}", self)
477    }
478}
479
480impl std::error::Error for StatisticsFromCsvBytesError {}
481
482//
483#[derive(Debug)]
484pub enum StatisticsFromJsonBytesError {
485    DeOutputFailed(SerdeJsonError),
486    DeFailed(SerdeJsonError),
487}
488
489impl fmt::Display for StatisticsFromJsonBytesError {
490    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491        write!(f, "{:?}", self)
492    }
493}
494
495impl std::error::Error for StatisticsFromJsonBytesError {}
496
497//
498//
499//
500#[derive(Deserialize, Debug, Clone)]
501pub struct JsonOutput(pub Vec<Vec<JsonOutputItem>>);
502
503#[derive(Deserialize, Debug, Clone)]
504pub struct JsonOutputItem {
505    #[serde(rename = "objType")]
506    pub obj_type: Box<str>,
507    #[serde(rename = "proxyId")]
508    pub proxy_id: usize,
509    pub id: usize,
510    pub field: json::Field,
511    #[serde(rename = "processNum")]
512    pub process_num: usize,
513    pub tags: json::Tags,
514    pub value: json::Value,
515}
516
517#[cfg(test)]
518mod tests {
519    use super::*;
520
521    use std::fs;
522
523    #[test]
524    fn test_statistics_from_csv_bytes() {
525        let bytes = include_bytes!("../tests/files/2_5_5_show_stat.csv");
526
527        let statistics = Statistics::from_csv_bytes(bytes).unwrap();
528
529        assert_eq!(statistics.len(), 12);
530        assert_eq!(
531            statistics[0].as_frontend().unwrap().pxname,
532            "http-frontend".into()
533        );
534        assert_eq!(
535            statistics[1].as_server().unwrap().svname,
536            "http-backend-srv-1".into()
537        );
538        assert_eq!(
539            statistics[2].as_backend().unwrap().pxname,
540            "http-backend".into()
541        );
542    }
543
544    #[test]
545    fn test_statistics_from_csv_bytes_with_match_files() {
546        for entry in fs::read_dir("tests/files").unwrap() {
547            let entry = entry.unwrap();
548            let path = entry.path();
549
550            if path.is_file() && path.to_str().unwrap().ends_with("show_stat.csv") {
551                let bytes = fs::read(&path).unwrap();
552
553                let _ = Statistics::from_csv_bytes(bytes).unwrap();
554
555                println!("file {:?}", path);
556            }
557        }
558    }
559
560    #[test]
561    fn test_statistics_from_json_bytes() {
562        let bytes = include_bytes!("../tests/files/2_5_5_show_stat.json");
563
564        let statistics = Statistics::from_json_bytes(bytes).unwrap();
565
566        assert_eq!(statistics.len(), 12);
567        assert_eq!(
568            statistics[0].as_frontend().unwrap().pxname,
569            "http-frontend".into()
570        );
571        assert_eq!(
572            statistics[1].as_server().unwrap().svname,
573            "http-backend-srv-1".into()
574        );
575        assert_eq!(
576            statistics[2].as_backend().unwrap().pxname,
577            "http-backend".into()
578        );
579    }
580}