use futures_util::{Stream, StreamExt};
use bollard::models::ContainerStatsResponse;
use bollard::query_parameters::StatsOptions;
use super::{DockerClient, describe};
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct StatsSample {
pub cpu_percent: f64,
pub memory_used: u64,
pub memory_limit: u64,
pub net_rx: u64,
pub net_tx: u64,
pub block_read: u64,
pub block_write: u64,
}
impl From<&ContainerStatsResponse> for StatsSample {
fn from(s: &ContainerStatsResponse) -> Self {
let cpu = s.cpu_stats.as_ref();
let pre = s.precpu_stats.as_ref();
let total = |c: Option<&bollard::models::ContainerCpuStats>| {
c.and_then(|c| c.cpu_usage.as_ref())
.and_then(|u| u.total_usage)
.unwrap_or(0)
};
let system = |c: Option<&bollard::models::ContainerCpuStats>| {
c.and_then(|c| c.system_cpu_usage).unwrap_or(0)
};
let cpu_delta = total(cpu).saturating_sub(total(pre)) as f64;
let system_delta = system(cpu).saturating_sub(system(pre)) as f64;
let cpus = cpu.and_then(|c| c.online_cpus).unwrap_or(1).max(1) as f64;
let cpu_percent = if cpu_delta > 0.0 && system_delta > 0.0 {
cpu_delta / system_delta * cpus * 100.0
} else {
0.0
};
let memory = s.memory_stats.as_ref();
let cache = memory
.and_then(|m| m.stats.as_ref())
.and_then(|st| st.get("inactive_file").or_else(|| st.get("cache")).copied())
.unwrap_or(0);
let memory_used = memory
.and_then(|m| m.usage)
.unwrap_or(0)
.saturating_sub(cache);
let memory_limit = memory.and_then(|m| m.limit).unwrap_or(0);
let (net_rx, net_tx) = s
.networks
.iter()
.flat_map(|nets| nets.values())
.fold((0, 0), |(rx, tx), n| {
(rx + n.rx_bytes.unwrap_or(0), tx + n.tx_bytes.unwrap_or(0))
});
let block = |op: &str| {
s.blkio_stats
.as_ref()
.and_then(|b| b.io_service_bytes_recursive.as_ref())
.map(|entries| {
entries
.iter()
.filter(|e| e.op.as_deref().is_some_and(|o| o.eq_ignore_ascii_case(op)))
.filter_map(|e| e.value)
.sum()
})
.unwrap_or(0)
};
Self {
cpu_percent,
memory_used,
memory_limit,
net_rx,
net_tx,
block_read: block("read"),
block_write: block("write"),
}
}
}
impl DockerClient {
pub async fn stats_once(&self, id: &str) -> Result<StatsSample, String> {
let opts = StatsOptions {
stream: false,
one_shot: false,
};
match self.docker.stats(id, Some(opts)).next().await {
Some(reading) => reading.map(|s| StatsSample::from(&s)).map_err(describe),
None => Err("no reading".into()),
}
}
pub fn stats(&self, id: &str) -> impl Stream<Item = Result<StatsSample, String>> + use<> {
let opts = StatsOptions {
stream: true,
one_shot: false,
};
self.docker
.stats(id, Some(opts))
.map(|reading| reading.map(|s| StatsSample::from(&s)).map_err(describe))
}
}
#[cfg(test)]
mod tests {
use super::*;
use bollard::models::{
ContainerBlkioStatEntry, ContainerBlkioStats, ContainerCpuStats, ContainerCpuUsage,
ContainerMemoryStats,
};
use std::collections::HashMap;
fn cpu(total: u64, system: u64) -> ContainerCpuStats {
ContainerCpuStats {
cpu_usage: Some(ContainerCpuUsage {
total_usage: Some(total),
..Default::default()
}),
system_cpu_usage: Some(system),
online_cpus: Some(4),
..Default::default()
}
}
#[test]
fn computes_like_docker_stats() {
let response = ContainerStatsResponse {
precpu_stats: Some(cpu(1_000, 10_000)),
cpu_stats: Some(cpu(1_500, 12_000)),
memory_stats: Some(ContainerMemoryStats {
usage: Some(300),
limit: Some(1_000),
stats: Some(HashMap::from([("inactive_file".to_string(), 100)])),
..Default::default()
}),
blkio_stats: Some(ContainerBlkioStats {
io_service_bytes_recursive: Some(vec![
ContainerBlkioStatEntry {
op: Some("read".into()),
value: Some(7),
..Default::default()
},
ContainerBlkioStatEntry {
op: Some("Write".into()),
value: Some(9),
..Default::default()
},
]),
..Default::default()
}),
..Default::default()
};
let sample = StatsSample::from(&response);
assert!((sample.cpu_percent - 100.0).abs() < 1e-9);
assert_eq!(sample.memory_used, 200);
assert_eq!(sample.memory_limit, 1_000);
assert_eq!((sample.block_read, sample.block_write), (7, 9));
}
#[test]
fn first_reading_has_no_cpu_delta() {
let sample = StatsSample::from(&ContainerStatsResponse::default());
assert_eq!(sample, StatsSample::default());
}
}