2021-10-13 08:24:41 +00:00
|
|
|
//! # Proxmox RRD format version 2
|
|
|
|
//!
|
|
|
|
//! The new format uses
|
|
|
|
//! [CBOR](https://datatracker.ietf.org/doc/html/rfc8949) as storage
|
|
|
|
//! format. This way we can use the serde serialization framework,
|
|
|
|
//! which make our code more flexible, much nicer and type safe.
|
|
|
|
//!
|
|
|
|
//! ## Features
|
|
|
|
//!
|
|
|
|
//! * Well defined data format [CBOR](https://datatracker.ietf.org/doc/html/rfc8949)
|
|
|
|
//! * Plattform independent (big endian f64, hopefully a standard format?)
|
|
|
|
//! * Arbitrary number of RRAs (dynamically changeable)
|
2021-10-06 10:19:54 +00:00
|
|
|
|
2020-05-23 07:29:33 +00:00
|
|
|
use std::path::Path;
|
|
|
|
|
2021-10-13 08:24:50 +00:00
|
|
|
use anyhow::{bail, format_err, Error};
|
2021-10-13 08:24:41 +00:00
|
|
|
use serde::{Serialize, Deserialize};
|
|
|
|
|
|
|
|
use proxmox::tools::fs::{replace_file, CreateOptions};
|
|
|
|
use proxmox_schema::api;
|
|
|
|
|
|
|
|
use crate::rrd_v1;
|
|
|
|
|
|
|
|
/// Proxmox RRD v2 file magic number
|
|
|
|
// openssl::sha::sha256(b"Proxmox Round Robin Database file v2.0")[0..8];
|
|
|
|
pub const PROXMOX_RRD_MAGIC_2_0: [u8; 8] = [224, 200, 228, 27, 239, 112, 122, 159];
|
|
|
|
|
|
|
|
#[api()]
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)]
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
/// RRD data source type
|
|
|
|
pub enum DST {
|
|
|
|
/// Gauge values are stored unmodified.
|
|
|
|
Gauge,
|
|
|
|
/// Stores the difference to the previous value.
|
|
|
|
Derive,
|
|
|
|
/// Stores the difference to the previous value (like Derive), but
|
|
|
|
/// detect counter overflow (and ignores that value)
|
|
|
|
Counter,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[api()]
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq)]
|
|
|
|
#[serde(rename_all = "kebab-case")]
|
|
|
|
/// Consolidation function
|
|
|
|
pub enum CF {
|
|
|
|
/// Average
|
|
|
|
Average,
|
|
|
|
/// Maximum
|
|
|
|
Maximum,
|
|
|
|
/// Minimum
|
|
|
|
Minimum,
|
2021-10-13 08:24:45 +00:00
|
|
|
/// Use the last value
|
|
|
|
Last,
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Serialize, Deserialize)]
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Data source specification
|
2021-10-13 08:24:41 +00:00
|
|
|
pub struct DataSource {
|
|
|
|
/// Data source type
|
|
|
|
pub dst: DST,
|
|
|
|
/// Last update time (epoch)
|
|
|
|
pub last_update: f64,
|
|
|
|
/// Stores the last value, used to compute differential value for
|
|
|
|
/// derive/counters
|
2021-10-13 08:24:51 +00:00
|
|
|
pub last_value: f64,
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
impl DataSource {
|
2021-10-06 05:06:17 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
pub fn new(dst: DST) -> Self {
|
|
|
|
Self {
|
|
|
|
dst,
|
|
|
|
last_update: 0.0,
|
2021-10-13 08:24:51 +00:00
|
|
|
last_value: f64::NAN,
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
fn compute_new_value(&mut self, time: f64, mut value: f64) -> Result<f64, Error> {
|
2021-10-13 08:24:50 +00:00
|
|
|
if time < 0.0 {
|
|
|
|
bail!("got negative time");
|
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
if time <= self.last_update {
|
|
|
|
bail!("time in past ({} < {})", time, self.last_update);
|
|
|
|
}
|
2020-05-25 07:21:54 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
if value.is_nan() {
|
|
|
|
bail!("new value is NAN");
|
|
|
|
}
|
2020-05-24 14:51:28 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
// derive counter value
|
|
|
|
let is_counter = self.dst == DST::Counter;
|
|
|
|
|
|
|
|
if is_counter || self.dst == DST::Derive {
|
|
|
|
let time_diff = time - self.last_update;
|
|
|
|
|
2021-10-13 08:24:51 +00:00
|
|
|
let diff = if self.last_value.is_nan() {
|
2021-10-13 08:24:41 +00:00
|
|
|
0.0
|
|
|
|
} else if is_counter && value < 0.0 {
|
|
|
|
bail!("got negative value for counter");
|
2021-10-13 08:24:51 +00:00
|
|
|
} else if is_counter && value < self.last_value {
|
2021-10-13 08:24:41 +00:00
|
|
|
// Note: We do not try automatic overflow corrections, but
|
2021-10-13 08:24:51 +00:00
|
|
|
// we update last_value anyways, so that we can compute the diff
|
2021-10-13 08:24:41 +00:00
|
|
|
// next time.
|
2021-10-13 08:24:51 +00:00
|
|
|
self.last_value = value;
|
2021-10-13 08:24:41 +00:00
|
|
|
bail!("conter overflow/reset detected");
|
|
|
|
} else {
|
2021-10-13 08:24:51 +00:00
|
|
|
value - self.last_value
|
2021-10-13 08:24:41 +00:00
|
|
|
};
|
2021-10-13 08:24:51 +00:00
|
|
|
self.last_value = value;
|
2021-10-13 08:24:41 +00:00
|
|
|
value = diff/time_diff;
|
2021-10-13 08:24:51 +00:00
|
|
|
} else {
|
|
|
|
self.last_value = value;
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
2020-05-24 14:51:28 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
Ok(value)
|
2020-05-24 14:51:28 +00:00
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
|
|
|
|
|
2020-05-24 14:51:28 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
#[derive(Serialize, Deserialize)]
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Round Robin Archive
|
2021-10-06 10:19:54 +00:00
|
|
|
pub struct RRA {
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Number of seconds spaned by a single data entry.
|
2021-10-06 10:19:54 +00:00
|
|
|
pub resolution: u64,
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Consolitation function.
|
2021-10-13 08:24:41 +00:00
|
|
|
pub cf: CF,
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Count values computed inside this update interval.
|
2021-10-06 10:19:54 +00:00
|
|
|
pub last_count: u64,
|
2021-10-14 09:53:54 +00:00
|
|
|
/// The actual data entries.
|
2021-10-13 08:24:41 +00:00
|
|
|
pub data: Vec<f64>,
|
2020-05-24 07:09:09 +00:00
|
|
|
}
|
|
|
|
|
2020-05-24 14:51:28 +00:00
|
|
|
impl RRA {
|
2021-10-13 08:24:41 +00:00
|
|
|
|
|
|
|
pub fn new(cf: CF, resolution: u64, points: usize) -> Self {
|
2020-05-24 14:51:28 +00:00
|
|
|
Self {
|
2021-10-13 08:24:41 +00:00
|
|
|
cf,
|
|
|
|
resolution,
|
2020-05-24 14:51:28 +00:00
|
|
|
last_count: 0,
|
2021-10-13 08:24:41 +00:00
|
|
|
data: vec![f64::NAN; points],
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:49 +00:00
|
|
|
pub fn slot_end_time(&self, time: u64) -> u64 {
|
|
|
|
self.resolution * (time / self.resolution + 1)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn slot_start_time(&self, time: u64) -> u64 {
|
|
|
|
self.resolution * (time / self.resolution)
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn slot(&self, time: u64) -> usize {
|
|
|
|
((time / self.resolution) as usize) % self.data.len()
|
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
// directly overwrite data slots
|
|
|
|
// the caller need to set last_update value on the DataSource manually.
|
2021-10-13 08:24:52 +00:00
|
|
|
pub fn insert_data(
|
2021-10-13 08:24:41 +00:00
|
|
|
&mut self,
|
|
|
|
start: u64,
|
|
|
|
resolution: u64,
|
|
|
|
data: Vec<Option<f64>>,
|
|
|
|
) -> Result<(), Error> {
|
|
|
|
if resolution != self.resolution {
|
|
|
|
bail!("inser_data failed: got wrong resolution");
|
|
|
|
}
|
2021-10-13 08:24:49 +00:00
|
|
|
|
|
|
|
let mut index = self.slot(start);
|
2021-10-13 08:24:41 +00:00
|
|
|
|
|
|
|
for i in 0..data.len() {
|
|
|
|
if let Some(v) = data[i] {
|
|
|
|
self.data[index] = v;
|
|
|
|
}
|
2021-10-13 08:24:48 +00:00
|
|
|
index += 1; if index >= self.data.len() { index = 0; }
|
2020-05-24 14:51:28 +00:00
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
Ok(())
|
2020-05-24 14:51:28 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
fn delete_old_slots(&mut self, time: f64, last_update: f64) {
|
2020-05-25 05:02:04 +00:00
|
|
|
let epoch = time as u64;
|
2021-10-13 08:24:41 +00:00
|
|
|
let last_update = last_update as u64;
|
2020-05-24 14:51:28 +00:00
|
|
|
let reso = self.resolution;
|
2021-10-13 08:24:41 +00:00
|
|
|
let num_entries = self.data.len() as u64;
|
2020-05-25 05:02:04 +00:00
|
|
|
|
2021-10-14 08:17:07 +00:00
|
|
|
let min_time = epoch.saturating_sub(num_entries*reso);
|
2021-10-14 14:29:00 +00:00
|
|
|
let min_time = self.slot_end_time(min_time);
|
2021-10-13 08:24:49 +00:00
|
|
|
|
2021-10-14 14:29:00 +00:00
|
|
|
let mut t = last_update.saturating_sub(num_entries*reso);
|
2021-10-13 08:24:49 +00:00
|
|
|
let mut index = self.slot(t);
|
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
for _ in 0..num_entries {
|
|
|
|
t += reso;
|
2021-10-13 08:24:48 +00:00
|
|
|
index += 1; if index >= self.data.len() { index = 0; }
|
2020-05-24 14:51:28 +00:00
|
|
|
if t < min_time {
|
|
|
|
self.data[index] = f64::NAN;
|
|
|
|
} else {
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
fn compute_new_value(&mut self, time: f64, last_update: f64, value: f64) {
|
2020-05-25 05:02:04 +00:00
|
|
|
let epoch = time as u64;
|
2021-10-13 08:24:41 +00:00
|
|
|
let last_update = last_update as u64;
|
2020-05-24 14:51:28 +00:00
|
|
|
let reso = self.resolution;
|
2020-05-25 05:02:04 +00:00
|
|
|
|
2021-10-13 08:24:49 +00:00
|
|
|
let index = self.slot(epoch);
|
|
|
|
let last_index = self.slot(last_update);
|
2020-05-24 14:51:28 +00:00
|
|
|
|
2021-10-13 08:24:49 +00:00
|
|
|
if (epoch - last_update) > reso || index != last_index {
|
2020-05-24 14:51:28 +00:00
|
|
|
self.last_count = 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
let last_value = self.data[index];
|
|
|
|
if last_value.is_nan() {
|
|
|
|
self.last_count = 0;
|
|
|
|
}
|
|
|
|
|
2021-10-14 14:10:55 +00:00
|
|
|
let new_count = self.last_count.saturating_add(1);
|
2020-05-24 15:03:02 +00:00
|
|
|
|
2020-05-24 14:51:28 +00:00
|
|
|
if self.last_count == 0 {
|
|
|
|
self.data[index] = value;
|
|
|
|
self.last_count = 1;
|
|
|
|
} else {
|
2021-10-13 08:24:41 +00:00
|
|
|
let new_value = match self.cf {
|
|
|
|
CF::Maximum => if last_value > value { last_value } else { value },
|
|
|
|
CF::Minimum => if last_value < value { last_value } else { value },
|
2021-10-13 08:24:45 +00:00
|
|
|
CF::Last => value,
|
2021-10-13 08:24:41 +00:00
|
|
|
CF::Average => {
|
|
|
|
(last_value*(self.last_count as f64))/(new_count as f64)
|
|
|
|
+ value/(new_count as f64)
|
|
|
|
}
|
2020-05-24 14:51:28 +00:00
|
|
|
};
|
|
|
|
self.data[index] = new_value;
|
|
|
|
self.last_count = new_count;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:52 +00:00
|
|
|
pub fn extract_data(
|
2020-05-23 07:29:33 +00:00
|
|
|
&self,
|
2021-10-13 08:24:41 +00:00
|
|
|
start: u64,
|
|
|
|
end: u64,
|
|
|
|
last_update: f64,
|
2020-05-23 13:37:17 +00:00
|
|
|
) -> (u64, u64, Vec<Option<f64>>) {
|
2021-10-13 08:24:41 +00:00
|
|
|
let last_update = last_update as u64;
|
|
|
|
let reso = self.resolution;
|
|
|
|
let num_entries = self.data.len() as u64;
|
2020-05-23 07:29:33 +00:00
|
|
|
|
|
|
|
let mut list = Vec::new();
|
|
|
|
|
2021-10-13 08:24:49 +00:00
|
|
|
let rrd_end = self.slot_end_time(last_update);
|
2021-10-13 08:24:41 +00:00
|
|
|
let rrd_start = rrd_end.saturating_sub(reso*num_entries);
|
2020-05-24 14:51:28 +00:00
|
|
|
|
2020-05-23 07:29:33 +00:00
|
|
|
let mut t = start;
|
2021-10-13 08:24:49 +00:00
|
|
|
let mut index = self.slot(t);
|
2021-10-13 08:24:41 +00:00
|
|
|
for _ in 0..num_entries {
|
|
|
|
if t > end { break; };
|
2021-10-14 08:17:07 +00:00
|
|
|
if t < rrd_start || t >= rrd_end {
|
2020-05-23 13:37:17 +00:00
|
|
|
list.push(None);
|
2020-05-23 07:29:33 +00:00
|
|
|
} else {
|
2021-10-13 08:24:41 +00:00
|
|
|
let value = self.data[index];
|
2020-05-24 07:09:09 +00:00
|
|
|
if value.is_nan() {
|
2020-05-23 13:37:17 +00:00
|
|
|
list.push(None);
|
2020-05-23 07:29:33 +00:00
|
|
|
} else {
|
2020-05-23 13:37:17 +00:00
|
|
|
list.push(Some(value));
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
|
|
|
}
|
2021-10-13 08:24:48 +00:00
|
|
|
t += reso;
|
|
|
|
index += 1; if index >= self.data.len() { index = 0; }
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
|
|
|
|
2020-10-14 09:18:26 +00:00
|
|
|
(start, reso, list)
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
#[derive(Serialize, Deserialize)]
|
2021-10-14 09:53:54 +00:00
|
|
|
/// Round Robin Database
|
2021-10-13 08:24:41 +00:00
|
|
|
pub struct RRD {
|
2021-10-14 09:53:54 +00:00
|
|
|
/// The data source definition
|
2021-10-13 08:24:41 +00:00
|
|
|
pub source: DataSource,
|
2021-10-14 09:53:54 +00:00
|
|
|
/// List of round robin archives
|
2021-10-13 08:24:41 +00:00
|
|
|
pub rra_list: Vec<RRA>,
|
|
|
|
}
|
2020-05-23 12:03:44 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
impl RRD {
|
2020-05-23 07:29:33 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
pub fn new(dst: DST, rra_list: Vec<RRA>) -> RRD {
|
|
|
|
|
|
|
|
let source = DataSource::new(dst);
|
|
|
|
|
|
|
|
RRD {
|
|
|
|
source,
|
|
|
|
rra_list,
|
2020-05-25 07:21:54 +00:00
|
|
|
}
|
|
|
|
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:50 +00:00
|
|
|
fn from_raw(raw: &[u8]) -> Result<Self, Error> {
|
2021-10-13 08:24:41 +00:00
|
|
|
if raw.len() < 8 {
|
2021-10-13 08:24:50 +00:00
|
|
|
bail!("not an rrd file - file is too small ({})", raw.len());
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
|
|
|
|
2021-10-13 08:24:50 +00:00
|
|
|
let rrd = if raw[0..8] == rrd_v1::PROXMOX_RRD_MAGIC_1_0 {
|
2021-10-13 08:24:41 +00:00
|
|
|
let v1 = rrd_v1::RRDv1::from_raw(&raw)?;
|
|
|
|
v1.to_rrd_v2()
|
2021-10-13 08:24:50 +00:00
|
|
|
.map_err(|err| format_err!("unable to convert from old V1 format - {}", err))?
|
2021-10-13 08:24:41 +00:00
|
|
|
} else if raw[0..8] == PROXMOX_RRD_MAGIC_2_0 {
|
|
|
|
serde_cbor::from_slice(&raw[8..])
|
2021-10-13 08:24:50 +00:00
|
|
|
.map_err(|err| format_err!("unable to decode RRD file - {}", err))?
|
|
|
|
} else {
|
|
|
|
bail!("not an rrd file - unknown magic number");
|
|
|
|
};
|
|
|
|
|
|
|
|
if rrd.source.last_update < 0.0 {
|
|
|
|
bail!("rrd file has negative last_update time");
|
|
|
|
}
|
|
|
|
|
|
|
|
Ok(rrd)
|
|
|
|
}
|
|
|
|
|
|
|
|
/// Load data from a file
|
|
|
|
pub fn load(path: &Path) -> Result<Self, std::io::Error> {
|
|
|
|
let raw = std::fs::read(path)?;
|
|
|
|
|
|
|
|
match Self::from_raw(&raw) {
|
|
|
|
Ok(rrd) => Ok(rrd),
|
|
|
|
Err(err) => Err(std::io::Error::new(std::io::ErrorKind::Other, err.to_string())),
|
2021-10-13 08:24:41 +00:00
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
2020-05-23 12:03:44 +00:00
|
|
|
|
2021-10-06 10:19:54 +00:00
|
|
|
/// Store data into a file (atomic replace file)
|
2021-10-06 05:06:17 +00:00
|
|
|
pub fn save(&self, filename: &Path, options: CreateOptions) -> Result<(), Error> {
|
2021-10-13 08:24:41 +00:00
|
|
|
let mut data: Vec<u8> = Vec::new();
|
|
|
|
data.extend(&PROXMOX_RRD_MAGIC_2_0);
|
|
|
|
serde_cbor::to_writer(&mut data, self)?;
|
|
|
|
replace_file(filename, &data, options)
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
2020-05-23 12:03:44 +00:00
|
|
|
|
2021-10-13 08:24:38 +00:00
|
|
|
pub fn last_update(&self) -> f64 {
|
2021-10-13 08:24:41 +00:00
|
|
|
self.source.last_update
|
2021-10-13 08:24:38 +00:00
|
|
|
}
|
|
|
|
|
2021-10-06 10:19:54 +00:00
|
|
|
/// Update the value (in memory)
|
|
|
|
///
|
|
|
|
/// Note: This does not call [Self::save].
|
2020-05-25 05:02:04 +00:00
|
|
|
pub fn update(&mut self, time: f64, value: f64) {
|
2020-05-24 04:44:06 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
let value = match self.source.compute_new_value(time, value) {
|
|
|
|
Ok(value) => value,
|
|
|
|
Err(err) => {
|
|
|
|
log::error!("rrd update failed: {}", err);
|
|
|
|
return;
|
2021-10-07 06:01:12 +00:00
|
|
|
}
|
|
|
|
};
|
2021-10-06 16:19:22 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
let last_update = self.source.last_update;
|
|
|
|
self.source.last_update = time;
|
2020-05-23 12:03:44 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
for rra in self.rra_list.iter_mut() {
|
|
|
|
rra.delete_old_slots(time, last_update);
|
|
|
|
rra.compute_new_value(time, last_update, value);
|
|
|
|
}
|
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
/// Extract data from the archive
|
|
|
|
///
|
|
|
|
/// This selects the RRA with specified [CF] and (minimum)
|
|
|
|
/// resolution, and extract data from `start` to `end`.
|
2021-10-13 08:24:42 +00:00
|
|
|
///
|
|
|
|
/// `start`: Start time. If not sepecified, we simply extract 10 data points.
|
|
|
|
/// `end`: End time. Default is to use the current time.
|
2021-10-13 08:24:41 +00:00
|
|
|
pub fn extract_data(
|
|
|
|
&self,
|
|
|
|
cf: CF,
|
|
|
|
resolution: u64,
|
2021-10-13 08:24:42 +00:00
|
|
|
start: Option<u64>,
|
|
|
|
end: Option<u64>,
|
2021-10-13 08:24:41 +00:00
|
|
|
) -> Result<(u64, u64, Vec<Option<f64>>), Error> {
|
|
|
|
|
|
|
|
let mut rra: Option<&RRA> = None;
|
|
|
|
for item in self.rra_list.iter() {
|
|
|
|
if item.cf != cf { continue; }
|
|
|
|
if item.resolution > resolution { continue; }
|
|
|
|
|
|
|
|
if let Some(current) = rra {
|
|
|
|
if item.resolution > current.resolution {
|
|
|
|
rra = Some(item);
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
rra = Some(item);
|
|
|
|
}
|
|
|
|
}
|
2020-05-23 12:03:44 +00:00
|
|
|
|
2021-10-13 08:24:41 +00:00
|
|
|
match rra {
|
2021-10-13 08:24:42 +00:00
|
|
|
Some(rra) => {
|
|
|
|
let end = end.unwrap_or_else(|| proxmox_time::epoch_f64() as u64);
|
|
|
|
let start = start.unwrap_or(end - 10*rra.resolution);
|
|
|
|
Ok(rra.extract_data(start, end, self.source.last_update))
|
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
None => bail!("unable to find RRA suitable ({:?}:{})", cf, resolution),
|
|
|
|
}
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
2021-10-13 08:24:41 +00:00
|
|
|
|
2020-05-23 07:29:33 +00:00
|
|
|
}
|
2021-10-14 08:17:07 +00:00
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
|
|
|
|
2021-10-14 08:55:12 +00:00
|
|
|
#[test]
|
|
|
|
fn basic_rra_maximum_gauge_test() -> Result<(), Error> {
|
|
|
|
let rra = RRA::new(CF::Maximum, 60, 5);
|
|
|
|
let mut rrd = RRD::new(DST::Gauge, vec![rra]);
|
|
|
|
|
|
|
|
for i in 2..10 {
|
|
|
|
rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Maximum, 60, Some(0), Some(5*60))?;
|
|
|
|
assert_eq!(start, 0);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [None, Some(3.0), Some(5.0), Some(7.0), Some(9.0)]);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn basic_rra_minimum_gauge_test() -> Result<(), Error> {
|
|
|
|
let rra = RRA::new(CF::Minimum, 60, 5);
|
|
|
|
let mut rrd = RRD::new(DST::Gauge, vec![rra]);
|
|
|
|
|
|
|
|
for i in 2..10 {
|
|
|
|
rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Minimum, 60, Some(0), Some(5*60))?;
|
|
|
|
assert_eq!(start, 0);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [None, Some(2.0), Some(4.0), Some(6.0), Some(8.0)]);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2021-10-14 08:17:07 +00:00
|
|
|
#[test]
|
|
|
|
fn basic_rra_last_gauge_test() -> Result<(), Error> {
|
|
|
|
let rra = RRA::new(CF::Last, 60, 5);
|
|
|
|
let mut rrd = RRD::new(DST::Gauge, vec![rra]);
|
|
|
|
|
|
|
|
for i in 2..10 {
|
|
|
|
rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
assert!(rrd.extract_data(CF::Average, 60, Some(0), Some(5*60)).is_err(), "CF::Average should not exist");
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Last, 60, Some(0), Some(20*60))?;
|
|
|
|
assert_eq!(start, 0);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [None, Some(3.0), Some(5.0), Some(7.0), Some(9.0)]);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn basic_rra_average_derive_test() -> Result<(), Error> {
|
|
|
|
let rra = RRA::new(CF::Average, 60, 5);
|
|
|
|
let mut rrd = RRD::new(DST::Derive, vec![rra]);
|
|
|
|
|
|
|
|
for i in 2..10 {
|
|
|
|
rrd.update((i as f64)*30.0, (i*60) as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(60), Some(5*60))?;
|
|
|
|
assert_eq!(start, 60);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [Some(1.0), Some(2.0), Some(2.0), Some(2.0), None]);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
fn basic_rra_average_gauge_test() -> Result<(), Error> {
|
|
|
|
let rra = RRA::new(CF::Average, 60, 5);
|
|
|
|
let mut rrd = RRD::new(DST::Gauge, vec![rra]);
|
|
|
|
|
|
|
|
for i in 2..10 {
|
|
|
|
rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(60), Some(5*60))?;
|
|
|
|
assert_eq!(start, 60);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [Some(2.5), Some(4.5), Some(6.5), Some(8.5), None]);
|
|
|
|
|
|
|
|
for i in 10..14 {
|
|
|
|
rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
}
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(60), Some(5*60))?;
|
|
|
|
assert_eq!(start, 60);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [None, Some(4.5), Some(6.5), Some(8.5), Some(10.5)]);
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(3*60), Some(8*60))?;
|
|
|
|
assert_eq!(start, 3*60);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [Some(6.5), Some(8.5), Some(10.5), Some(12.5), None]);
|
|
|
|
|
|
|
|
// add much newer vaule (should delete all previous/outdated value)
|
|
|
|
let i = 100; rrd.update((i as f64)*30.0, i as f64);
|
|
|
|
println!("TEST {:?}", serde_json::to_string_pretty(&rrd));
|
|
|
|
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(100*30), Some(100*30 + 5*60))?;
|
|
|
|
assert_eq!(start, 100*30);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, [Some(100.0), None, None, None, None]);
|
|
|
|
|
|
|
|
// extract with end time smaller than start time
|
|
|
|
let (start, reso, data) = rrd.extract_data(CF::Average, 60, Some(100*30), Some(60))?;
|
|
|
|
assert_eq!(start, 100*30);
|
|
|
|
assert_eq!(reso, 60);
|
|
|
|
assert_eq!(data, []);
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
}
|