commit f7069473fff7afeb34d24165edc2439bf9545d72 Author: glyph Date: Mon Dec 6 14:21:56 2021 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..77bc511 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,100 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "mini-internal" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "926fbf1cd8695183e4712d1c77cdf2c5f7916f9f73ba103dca78ff2e6755ab47" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "miniserde" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0767e42acf28c5e08fee73cb657bcb23432c3769cbdf3881a8cb69d8df5020df" +dependencies = [ + "itoa", + "mini-internal", + "ryu", +] + +[[package]] +name = "proc-macro2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb37d2df5df740e582f28f8560cf425f52bb267d872fe58358eadb554909f07a" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "ryu" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9613b5a66ab9ba26415184cfc41156594925a9cf3a2057e57f31ff145f6568" + +[[package]] +name = "serde" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.130" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8daf5dd0bb60cbd4137b1b587d2fc0ae729bc07cf01cd70b36a1ed5ade3b9d59" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "vnstat_parse" +version = "0.1.0" +dependencies = [ + "miniserde", + "serde", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..88a7a8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "vnstat_parse" +version = "0.1.0" +authors = ["Andrew Reid Show traffic summary for selected interface using one line with a parsable + format. The output contains 15 fields with ; used as field delimiter. The + 1st field contains the API version information of the output that will only + be changed in future versions if the field content or structure changes. + The following fields in order 2) interface name, 3) timestamp for today, 4) + rx for today, 5) tx for today, 6) total for today, 7) average traffic rate + for today, 8) timestamp for current month, 9) rx for current month, 10) tx + for current month, 11) total for current month, 12) average traffic rate + for current month, 13) all time total rx, 14) all time total tx, 15) all + time total traffic. An optional mode parameter can be used to force all + fields to output in bytes without the unit itself shown. + +**Example output** + +```bash +vnstat eno1 --oneline +1;eno1;2021-11-29;6.02 GiB;0.99 GiB;7.00 GiB;738.84 kbit/s;2021-11;6.02 GiB;0.99 GiB;7.00 GiB;24.06 kbit/s;6.02 GiB;0.99 GiB;7.00 GiB +``` + +## Library Usage + +```rust +use vnstat_parse::{Error, Vnstat}; + +fn main() -> Result<(), Error> { + let vnstat_data = Vnstat::get("eno1")?; + + println!("{:?}", vnstat_data); + + Ok(()) +} +``` + +**Example output** + +```bash +Vnstat { iface: "eno1", today: "2021-11-29", day_rx: 6.02, day_rx_unit: "GiB", day_tx: 0.99, day_tx_unit: "GiB", day_total: 7.0, day_total_unit: "GiB", day_avg_rate: 738.84, day_avg_rate_unit: "kbit/s", month: "2021-11", month_rx: 6.02, month_rx_unit: "GiB", month_tx: 0.99, month_tx_unit: "GiB", month_total: 7.0, month_total_unit: "GiB", month_avg_rate: 24.06, month_avg_rate_unit: "kbit/s", all_time_rx: 6.02, all_time_rx_unit: "GiB", all_time_tx: 0.99, all_time_tx_unit: "GiB", all_time_total: 7.0, all_time_total_unit: "GiB" } +``` + +## Optional Features + +`Serialize` and `Deserialize` can be optionally derived for the `Vnstat` `struct` using either [miniserde](https://crates.io/crates/miniserde) or [serde](https://crates.io/crates/serde). These features are disabled by default to offer a zero dependency parser. `miniserde` offers a lightweight option when compared with `serde` (one less dependency and shorter compile times). + +Specify the desired feature in your `Cargo.toml` manifest: + +```toml +vnstat_parse = { version = "0.1", features = ["miniserde"] } +``` + +## License + +MIT. diff --git a/examples/vnstat.rs b/examples/vnstat.rs new file mode 100644 index 0000000..1fdf5e3 --- /dev/null +++ b/examples/vnstat.rs @@ -0,0 +1,15 @@ +use vnstat_parse::{Error, Vnstat}; + +fn main() -> Result<(), Error> { + // supply the name of the desired interface + let vnstat_data = Vnstat::get("eno1")?; + + println!("{:?}", vnstat_data); + + println!( + "Total network traffic for today: {} {}", + vnstat_data.day_total, vnstat_data.day_total_unit + ); + + Ok(()) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..7368a22 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,257 @@ +#![warn(missing_docs)] + +//! A library to parse oneline data output from `vnstat`. +//! +//! All fields are parsed, with the exception of the API version information. +//! Dates are parsed to `String`, data values are parsed to `f32` and data units +//! are parsed to `String`. +//! +//! Here is the summary of the `--oneline` option from the `vnstat man` page: +//! +//! > Show traffic summary for selected interface using one line with a parsable +//! format. The output contains 15 fields with ; used as field delimiter. The +//! 1st field contains the API version information of the output that will only +//! be changed in future versions if the field content or structure changes. +//! The following fields in order 2) interface name, 3) timestamp for today, 4) +//! rx for today, 5) tx for today, 6) total for today, 7) average traffic rate +//! for today, 8) timestamp for current month, 9) rx for current month, 10) tx +//! for current month, 11) total for current month, 12) average traffic rate +//! for current month, 13) all time total rx, 14) all time total tx, 15) all +//! time total traffic. An optional mode parameter can be used to force all +//! fields to output in bytes without the unit itself shown. +//! +//! Example output: +//! +//! `1;eno1;2021-11-29;6.02 GiB;0.99 GiB;7.00 GiB;738.84 kbit/s;2021-11;6.02 GiB;0.99 GiB;7.00 GiB;24.06 kbit/s;6.02 GiB;0.99 GiB;7.00 GiB` +//! +//! ## Library Usage +//! +//! ```rust +//! use vnstat_parse::{Error, Vnstat}; +//! +//! fn main() -> Result<(), Error> { +//! let vnstat_data = Vnstat::get("eno1")?; +//! +//! println!("{:?}", vnstat_data); +//! +//! Ok(()) +//! } +//! ``` +//! +//! **Example output** +//! +//! ```bash +//! Vnstat { iface: "eno1", today: "2021-11-29", day_rx: 6.02, day_rx_unit: "GiB", day_tx: 0.99, day_tx_unit: "GiB", day_total: 7.0, day_total_unit: "GiB", day_avg_rate: 738.84, day_avg_rate_unit: "kbit/s", month: "2021-11", month_rx: 6.02, month_rx_unit: "GiB", month_tx: 0.99, month_tx_unit: "GiB", month_total: 7.0, month_total_unit: "GiB", month_avg_rate: 24.06, month_avg_rate_unit: "kbit/s", all_time_rx: 6.02, all_time_rx_unit: "GiB", all_time_tx: 0.99, all_time_tx_unit: "GiB", all_time_total: 7.0, all_time_total_unit: "GiB" } +//! ``` +//! +//! ## Optional Features +//! +//! `Serialize` and `Deserialize` can be optionally derived for the `Vnstat` +//! `struct` using either [miniserde](https://crates.io/crates/miniserde) or +//! [serde](https://crates.io/crates/serde). These features are disabled by +//! default to offer a zero dependency parser. `miniserde` offers a lightweight +//! option when compared with `serde` (one less dependency and shorter compile +//! times). +//! +//! Specify the desired feature in your `Cargo.toml` manifest: +//! +//! ```toml +//! vnstat_parse = { version = "0.1", features = ["miniserde"] } +//! ``` + +use std::{ + io::Error as IoError, num::ParseFloatError, process::Command, result::Result, str::FromStr, +}; + +#[cfg(feature = "miniserde_support")] +use miniserde::{Deserialize, Serialize}; + +#[cfg(feature = "serde_support")] +use serde::{Deserialize, Serialize}; + +/// Custom error type encapsulating all possible errors for this library. +/// `From` implementations are provided for external error types. +#[derive(Debug)] +pub enum Error { + /// Failed to find ' ' (space) while parsing traffic data. + Find, + /// IO error. + Io(IoError), + /// Failed to parse a string slice to `f32`. + ParseFloat(ParseFloatError), + /// Stderr output from `vnstat` command. + StdErr(String), +} + +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match *self { + Error::Find => None, + Error::Io(ref err) => Some(err), + Error::ParseFloat(ref err) => Some(err), + Error::StdErr(_) => None, + } + } +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Error::Find => write!( + f, + "Find error: failed to find ' ' character (space) while parsing traffic data and unit" + ), + Error::Io(_) => write!(f, "IO error: failed to execute `vnstat` command"), + Error::ParseFloat(_) => write!(f, "Parse error: failed to parse float from string"), + Error::StdErr(ref err) => write!(f, "`vnstat` error: {}", err), + } + } +} + +impl From for Error { + fn from(err: IoError) -> Self { + Error::Io(err) + } +} + +impl From for Error { + fn from(err: ParseFloatError) -> Self { + Error::ParseFloat(err) + } +} + +/// Parsed network usage data for a single interface (sourced from the `vnstat` database). +#[derive(Debug)] +#[cfg_attr(feature = "miniserde_support", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "serde_support", derive(Serialize, Deserialize))] +pub struct Vnstat { + /// Network interface. + pub iface: String, + /// Timestamp for today (yyyy-mm-dd). + pub today: String, + /// Received data total for today. + pub day_rx: f32, + /// Unit of received data total for today. + pub day_rx_unit: String, + /// Transmitted data total for today. + pub day_tx: f32, + /// Unit of transmitted data total for today. + pub day_tx_unit: String, + /// Combined data total for today. + pub day_total: f32, + /// Unit of combined data total for today. + pub day_total_unit: String, + /// Average traffic rate for today. + pub day_avg_rate: f32, + /// Unit of average traffic rate for today. + pub day_avg_rate_unit: String, + /// Timestamp for current month (yyyy-mm). + pub month: String, + /// Received data for the current month. + pub month_rx: f32, + /// Unit of received data for the current month. + pub month_rx_unit: String, + /// Transmitted data for the current month. + pub month_tx: f32, + /// Unit of transmitted data for the current month. + pub month_tx_unit: String, + /// Combined data total for the current month. + pub month_total: f32, + /// Unit of combined data total for the current month. + pub month_total_unit: String, + /// Average traffic rate for the current month. + pub month_avg_rate: f32, + /// Unit of average traffic rate for the current month. + pub month_avg_rate_unit: String, + /// Received data for all time. + pub all_time_rx: f32, + /// Unit of received data for all time. + pub all_time_rx_unit: String, + /// Transmitted data for all time. + pub all_time_tx: f32, + /// Unit of transmitted data for all time. + pub all_time_tx_unit: String, + /// Combined data total for all time. + pub all_time_total: f32, + /// Unit of combined data total for all time. + pub all_time_total_unit: String, +} + +impl Vnstat { + /// Call `vnstat --online` with the given interface. If the command executes + /// succesfully, parse the `stdout` data. Otherwise, return `stderr`. + pub fn get(iface: &str) -> Result { + let vnstat_output = Command::new("vnstat") + .arg(&iface) + .arg("--oneline") + .output()?; + + match vnstat_output.status.success() { + true => { + let raw_data = String::from_utf8_lossy(&vnstat_output.stdout); + parse_vnstat_data(&raw_data) + } + false => Err(Error::StdErr( + String::from_utf8_lossy(&vnstat_output.stderr).to_string(), + )), + } + } +} + +/// Parse the `stdout` data from the `vnstat --oneline` command. +pub fn parse_vnstat_data(raw_data: &str) -> Result { + let data_vec: Vec<&str> = raw_data.trim().split(';').collect(); + + // find the position of the space (' ') in each element (set to `0` if none is found) + let space_vec: Vec = data_vec + .iter() + .map(|element| element.find(' ').unwrap_or(0)) + .collect(); + + // day data + let (day_rx, day_rx_unit) = data_vec[3].split_at(space_vec[3]); + let (day_tx, day_tx_unit) = data_vec[4].split_at(space_vec[4]); + let (day_total, day_total_unit) = data_vec[5].split_at(space_vec[5]); + let (day_avg_rate, day_avg_rate_unit) = data_vec[6].split_at(space_vec[6]); + + // month data + let (month_rx, month_rx_unit) = data_vec[8].split_at(space_vec[8]); + let (month_tx, month_tx_unit) = data_vec[9].split_at(space_vec[9]); + let (month_total, month_total_unit) = data_vec[10].split_at(space_vec[10]); + let (month_avg_rate, month_avg_rate_unit) = data_vec[11].split_at(space_vec[11]); + + // all time data + let (all_time_rx, all_time_rx_unit) = data_vec[12].split_at(space_vec[12]); + let (all_time_tx, all_time_tx_unit) = data_vec[13].split_at(space_vec[13]); + let (all_time_total, all_time_total_unit) = data_vec[14].split_at(space_vec[14]); + + let parsed_data = Vnstat { + iface: data_vec[1].to_string(), + today: data_vec[2].to_string(), + day_rx: f32::from_str(day_rx)?, + day_rx_unit: day_rx_unit.trim().to_string(), + day_tx: f32::from_str(day_tx)?, + day_tx_unit: day_tx_unit.trim().to_string(), + day_total: f32::from_str(day_total)?, + day_total_unit: day_total_unit.trim().to_string(), + day_avg_rate: f32::from_str(day_avg_rate)?, + day_avg_rate_unit: day_avg_rate_unit.trim().to_string(), + month: data_vec[7].to_string(), + month_rx: f32::from_str(month_rx)?, + month_rx_unit: month_rx_unit.trim().to_string(), + month_tx: f32::from_str(month_tx)?, + month_tx_unit: month_tx_unit.trim().to_string(), + month_total: f32::from_str(month_total)?, + month_total_unit: month_total_unit.trim().to_string(), + month_avg_rate: f32::from_str(month_avg_rate)?, + month_avg_rate_unit: month_avg_rate_unit.trim().to_string(), + all_time_rx: f32::from_str(all_time_rx)?, + all_time_rx_unit: all_time_rx_unit.trim().to_string(), + all_time_tx: f32::from_str(all_time_tx)?, + all_time_tx_unit: all_time_tx_unit.trim().to_string(), + all_time_total: f32::from_str(all_time_total)?, + all_time_total_unit: all_time_total_unit.trim().to_string(), + }; + + Ok(parsed_data) +}