mirror of
https://codeberg.org/icewind/palantir.git
synced 2026-06-03 18:24:08 +02:00
prepare for windows impl
This commit is contained in:
parent
f363cac81d
commit
d12b70d11e
16 changed files with 550 additions and 279 deletions
210
src/linux/disk/mod.rs
Normal file
210
src/linux/disk/mod.rs
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
use crate::{Error, MultiSensorSource, Result, SensorData};
|
||||
use ahash::{AHashSet, AHasher};
|
||||
use regex::Regex;
|
||||
use std::ffi::CString;
|
||||
use std::fmt::Write;
|
||||
use std::fs::File;
|
||||
use std::hash::{Hash, Hasher};
|
||||
use std::io::{Read, Seek};
|
||||
use std::mem::MaybeUninit;
|
||||
|
||||
pub mod zfs;
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct DiskStats {
|
||||
pub interface: String,
|
||||
pub bytes_sent: u64,
|
||||
pub bytes_received: u64,
|
||||
}
|
||||
|
||||
impl SensorData for DiskStats {
|
||||
fn write<W: Write>(&self, mut w: W, hostname: &str) {
|
||||
if self.bytes_received > 0 || self.bytes_sent > 0 {
|
||||
writeln!(
|
||||
&mut w,
|
||||
"disk_sent{{host=\"{}\", disk=\"{}\"}} {}",
|
||||
hostname, self.interface, self.bytes_sent
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut w,
|
||||
"disk_received{{host=\"{}\", disk=\"{}\"}} {}",
|
||||
hostname, self.interface, self.bytes_received
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskStatSource {
|
||||
source: File,
|
||||
buff: String,
|
||||
regex: Regex,
|
||||
}
|
||||
|
||||
impl DiskStatSource {
|
||||
pub fn new() -> Result<DiskStatSource> {
|
||||
Ok(DiskStatSource {
|
||||
source: File::open("/proc/diskstats")?,
|
||||
buff: String::new(),
|
||||
regex: Regex::new(r" ([sv]d[a-z]+|nvme[0-9]n[0-9]|mmcblk[0-9]) ").unwrap(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiSensorSource for DiskStatSource {
|
||||
type Data = DiskStats;
|
||||
type Iter<'a> = DiskStatParser<'a>;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Iter<'_>> {
|
||||
self.buff.clear();
|
||||
self.source.rewind()?;
|
||||
self.source.read_to_string(&mut self.buff)?;
|
||||
|
||||
Ok(DiskStatParser {
|
||||
lines: self.buff.lines(),
|
||||
regex: &self.regex,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskStatParser<'a> {
|
||||
lines: std::str::Lines<'a>,
|
||||
regex: &'a Regex,
|
||||
}
|
||||
|
||||
impl Iterator for DiskStatParser<'_> {
|
||||
type Item = Result<DiskStats>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let line = loop {
|
||||
let line = self.lines.next()?;
|
||||
if self.regex.is_match(line) {
|
||||
break line;
|
||||
}
|
||||
};
|
||||
let mut parts = line.split_whitespace().skip(2);
|
||||
let name: String = parts.next()?.into();
|
||||
let _read_count = parts.next();
|
||||
let _read_merged_count = parts.next();
|
||||
let read_sectors = parts.next()?.parse::<u64>().ok()?;
|
||||
let mut parts = parts.skip(1);
|
||||
let _write_count = parts.next();
|
||||
let _write_merged_count = parts.next();
|
||||
let write_sectors = parts.next()?.parse::<u64>().ok()?;
|
||||
Some(Ok(DiskStats {
|
||||
interface: name,
|
||||
bytes_sent: write_sectors * 512,
|
||||
bytes_received: read_sectors * 512,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DiskUsage {
|
||||
pub name: String,
|
||||
pub size: u64,
|
||||
pub free: u64,
|
||||
}
|
||||
|
||||
impl SensorData for DiskUsage {
|
||||
fn write<W: Write>(&self, mut w: W, hostname: &str) {
|
||||
if self.size > 0 {
|
||||
writeln!(
|
||||
&mut w,
|
||||
"disk_size{{host=\"{}\", disk=\"{}\"}} {}",
|
||||
hostname, self.name, self.size
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut w,
|
||||
"disk_free{{host=\"{}\", disk=\"{}\"}} {}",
|
||||
hostname, self.name, self.free
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskUsageSource {
|
||||
source: File,
|
||||
buff: String,
|
||||
}
|
||||
|
||||
impl DiskUsageSource {
|
||||
pub fn new() -> Result<DiskUsageSource> {
|
||||
Ok(DiskUsageSource {
|
||||
source: File::open("/proc/mounts")?,
|
||||
buff: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiSensorSource for DiskUsageSource {
|
||||
type Data = DiskUsage;
|
||||
type Iter<'a> = DiskUsageParser<'a>;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Iter<'_>> {
|
||||
self.buff.clear();
|
||||
self.source.rewind()?;
|
||||
self.source.read_to_string(&mut self.buff)?;
|
||||
|
||||
Ok(DiskUsageParser {
|
||||
lines: self.buff.lines(),
|
||||
found_disks: AHashSet::with_capacity(16),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DiskUsageParser<'a> {
|
||||
lines: std::str::Lines<'a>,
|
||||
found_disks: AHashSet<u64>,
|
||||
}
|
||||
|
||||
impl Iterator for DiskUsageParser<'_> {
|
||||
type Item = Result<DiskUsage>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let line = loop {
|
||||
let line = self.lines.next()?;
|
||||
if line.starts_with('/') && !line.contains("/dev/loop") && !line.contains("fuse") {
|
||||
break line;
|
||||
}
|
||||
};
|
||||
|
||||
let mut parts = line.split_ascii_whitespace();
|
||||
let disk = parts.next()?;
|
||||
if !self.found_disks.insert(hash_str(disk)) {
|
||||
return None;
|
||||
}
|
||||
let mount_point = parts.next()?;
|
||||
let stat = match statvfs(&mount_point) {
|
||||
Ok(stat) => stat,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
Some(Ok(DiskUsage {
|
||||
name: mount_point.to_string(),
|
||||
size: stat.f_blocks * stat.f_frsize as u64,
|
||||
free: stat.f_bavail * stat.f_frsize as u64,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
fn statvfs(path: &str) -> Result<libc::statvfs> {
|
||||
let path = CString::new(path)?;
|
||||
let mut vfs = MaybeUninit::<libc::statvfs>::uninit();
|
||||
let result = unsafe { libc::statvfs(path.as_ptr(), vfs.as_mut_ptr()) };
|
||||
|
||||
if result == 0 {
|
||||
let vfs = unsafe { vfs.assume_init() };
|
||||
Ok(vfs)
|
||||
} else {
|
||||
Err(Error::StatVfs)
|
||||
}
|
||||
}
|
||||
|
||||
fn hash_str(s: &str) -> u64 {
|
||||
let mut hasher = AHasher::default();
|
||||
s.hash(&mut hasher);
|
||||
hasher.finish()
|
||||
}
|
||||
154
src/linux/disk/zfs.rs
Normal file
154
src/linux/disk/zfs.rs
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
use crate::linux::disk::DiskUsage;
|
||||
use crate::Result;
|
||||
use std::fmt::Write;
|
||||
use std::fs::read_to_string;
|
||||
use std::process::Command;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tracing::warn;
|
||||
|
||||
static CAN_READ: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
pub fn pools() -> impl Iterator<Item = DiskUsage> {
|
||||
if !CAN_READ.load(Ordering::Relaxed) {
|
||||
return ZPoolOutputParser::default();
|
||||
}
|
||||
|
||||
ZPoolOutputParser {
|
||||
str: zpool_command().unwrap_or_default(),
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn zpool_command() -> Result<String> {
|
||||
let mut z = Command::new("zpool");
|
||||
z.args(&["list", "-p", "-H", "-o", "name,size,free"]);
|
||||
let out = z.output()?;
|
||||
if out.status.success() {
|
||||
Ok(String::from_utf8(out.stdout)?)
|
||||
} else {
|
||||
CAN_READ.store(false, Ordering::Relaxed);
|
||||
warn!(
|
||||
status = out.status.code().unwrap_or(-1),
|
||||
stdout = String::from_utf8(out.stdout).unwrap_or_else(|_| String::from("non utf8")),
|
||||
stderr = String::from_utf8(out.stderr).unwrap_or_else(|_| String::from("non utf8")),
|
||||
"Failed to list zpool status"
|
||||
);
|
||||
Ok(String::new())
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_line(line: &str) -> Option<DiskUsage> {
|
||||
let mut parts = line.split_ascii_whitespace();
|
||||
let name = parts.next()?.to_string();
|
||||
let size = parts.next()?.parse().ok()?;
|
||||
let free = parts.next()?.parse().ok()?;
|
||||
Some(DiskUsage { name, size, free })
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ZPoolOutputParser {
|
||||
str: String,
|
||||
pos: usize,
|
||||
}
|
||||
|
||||
impl Iterator for ZPoolOutputParser {
|
||||
type Item = DiskUsage;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let str = self.str.as_str();
|
||||
let line = match str[self.pos..].find('\n') {
|
||||
Some(next_pos) => {
|
||||
let old_pos = self.pos;
|
||||
self.pos += next_pos + 1;
|
||||
Some(&str[old_pos..self.pos])
|
||||
}
|
||||
None if self.pos < str.len() => {
|
||||
let old_pos = self.pos;
|
||||
self.pos = str.len();
|
||||
Some(&str[old_pos..])
|
||||
}
|
||||
None => None,
|
||||
};
|
||||
line.and_then(parse_line)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct ArcStats {
|
||||
hits: u64,
|
||||
misses: u64,
|
||||
prefetch: u64,
|
||||
size: u64,
|
||||
}
|
||||
|
||||
impl ArcStats {
|
||||
pub fn write<W: Write>(&self, mut w: W, hostname: &str) {
|
||||
writeln!(
|
||||
&mut w,
|
||||
"zfs_arc_hits{{host=\"{}\"}} {}",
|
||||
hostname, self.hits
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut w,
|
||||
"zfs_arc_misses{{host=\"{}\"}} {}",
|
||||
hostname, self.misses
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut w,
|
||||
"zfs_arc_size{{host=\"{}\"}} {}",
|
||||
hostname, self.size
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut w,
|
||||
"zfs_arc_prefetch{{host=\"{}\"}} {}",
|
||||
hostname, self.prefetch
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arcstats() -> Result<Option<ArcStats>> {
|
||||
let content = match read_to_string("/proc/spl/kstat/zfs/arcstats") {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
let mut stats = ArcStats::default();
|
||||
|
||||
for line in content.lines().skip(2) {
|
||||
let mut parts = line.split_ascii_whitespace();
|
||||
if let (Some(name), _, Some(Ok(value))) =
|
||||
(parts.next(), parts.next(), parts.next().map(u64::from_str))
|
||||
{
|
||||
match name {
|
||||
"demand_data_hits" => stats.hits += value,
|
||||
"demand_metadata_hits" => stats.hits += value,
|
||||
"prefetch_data_hits" => {
|
||||
stats.hits += value;
|
||||
stats.prefetch += value;
|
||||
}
|
||||
"prefetch_metadata_hits" => {
|
||||
stats.hits += value;
|
||||
stats.prefetch += value;
|
||||
}
|
||||
"demand_data_misses" => stats.misses += value,
|
||||
"demand_metadata_misses" => stats.misses += value,
|
||||
"prefetch_data_misses" => {
|
||||
stats.misses += value;
|
||||
stats.prefetch += value;
|
||||
}
|
||||
"prefetch_metadata_misses" => {
|
||||
stats.misses += value;
|
||||
stats.prefetch += value;
|
||||
}
|
||||
"size" => stats.size = value,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(stats))
|
||||
}
|
||||
88
src/linux/gpu/mod.rs
Normal file
88
src/linux/gpu/mod.rs
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
use crate::data::{GpuMemory, GpuUsage};
|
||||
use crate::linux::hwmon::FileSource;
|
||||
use std::fs::read_to_string;
|
||||
use std::str::FromStr;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::Mutex;
|
||||
use std::thread::sleep;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
pub mod nvidia;
|
||||
|
||||
fn read_num<T: FromStr>(path: &str) -> Option<T> {
|
||||
read_to_string(path).ok()?.trim().parse().ok()
|
||||
}
|
||||
|
||||
pub fn memory() -> Option<GpuMemory> {
|
||||
if let Some(nv_mem) = nvidia::memory() {
|
||||
return Some(nv_mem);
|
||||
}
|
||||
// 1 gpu should be enough for everyone
|
||||
let used = read_num::<u64>("/sys/class/drm/card0/device/mem_info_vram_used")?;
|
||||
let total = read_num("/sys/class/drm/card0/device/mem_info_vram_total")?;
|
||||
Some(GpuMemory {
|
||||
total,
|
||||
free: total - used,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn utilization() -> impl Iterator<Item = GpuUsage> {
|
||||
let nv_usage = nvidia::utilization();
|
||||
|
||||
let sources = [
|
||||
(
|
||||
"memory",
|
||||
read_num("/sys/class/drm/card0/device/mem_busy_percent"),
|
||||
),
|
||||
(
|
||||
"compute",
|
||||
read_num("/sys/class/drm/card0/device/gpu_busy_percent"),
|
||||
),
|
||||
];
|
||||
let drm = sources.into_iter().flat_map(|(system, usage)| {
|
||||
Some(GpuUsage {
|
||||
system,
|
||||
usage: usage?,
|
||||
})
|
||||
});
|
||||
drm.chain(nv_usage)
|
||||
}
|
||||
|
||||
static GPU_POWER_UJ: AtomicU64 = AtomicU64::new(0);
|
||||
static GPU_POWER_LAST_READ: Mutex<Option<Instant>> = Mutex::new(None);
|
||||
|
||||
fn get_gpu_power_elapsed() -> Option<Duration> {
|
||||
let mut last_read = GPU_POWER_LAST_READ.lock().unwrap();
|
||||
let now = Instant::now();
|
||||
let elapsed = last_read.as_ref().map(|last_read| now - *last_read);
|
||||
*last_read = Some(now);
|
||||
elapsed
|
||||
}
|
||||
|
||||
pub fn update_gpu_power() {
|
||||
if let Ok(mut file) =
|
||||
FileSource::open("/sys/class/drm/card0/device/hwmon/hwmon0/power1_average")
|
||||
{
|
||||
loop {
|
||||
if let Some(elapsed) = get_gpu_power_elapsed() {
|
||||
let current_power: u64 = match file.read() {
|
||||
Ok(current_power) => current_power,
|
||||
Err(_) => {
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let elapsed_milli = elapsed.as_millis() as u64;
|
||||
|
||||
let power = current_power * elapsed_milli / 1000;
|
||||
|
||||
GPU_POWER_UJ.fetch_add(power, Ordering::SeqCst);
|
||||
}
|
||||
sleep(Duration::from_millis(500));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn gpu_power() -> u64 {
|
||||
GPU_POWER_UJ.load(Ordering::SeqCst)
|
||||
}
|
||||
56
src/linux/gpu/nvidia.rs
Normal file
56
src/linux/gpu/nvidia.rs
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
use crate::data::{GpuMemory, GpuUsage};
|
||||
use nvml_wrapper::enum_wrappers::device::TemperatureSensor;
|
||||
use nvml_wrapper::{Device, Nvml};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
static NVIDIA: Lazy<Option<Nvml>> = Lazy::new(|| Nvml::init().ok());
|
||||
|
||||
fn device() -> Option<Device<'static>> {
|
||||
NVIDIA.as_ref()?.device_by_index(0).ok()
|
||||
}
|
||||
|
||||
pub fn temperature() -> Option<f32> {
|
||||
let temp = device()?.temperature(TemperatureSensor::Gpu).ok()?;
|
||||
Some(temp as f32)
|
||||
}
|
||||
|
||||
pub fn power() -> Option<u64> {
|
||||
device()?
|
||||
.total_energy_consumption()
|
||||
.ok()
|
||||
.map(|mj| mj * 1_000)
|
||||
}
|
||||
|
||||
pub fn memory() -> Option<GpuMemory> {
|
||||
let mem = device()?.memory_info().ok()?;
|
||||
Some(GpuMemory {
|
||||
total: mem.total,
|
||||
free: mem.free,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn utilization() -> impl Iterator<Item = GpuUsage> {
|
||||
let sources = if let Some(device) = device() {
|
||||
let utilization = device.utilization_rates().ok();
|
||||
[
|
||||
("compute", utilization.as_ref().map(|u| u.gpu)),
|
||||
("memory", utilization.as_ref().map(|u| u.gpu)),
|
||||
(
|
||||
"encode",
|
||||
device.encoder_utilization().ok().map(|u| u.utilization),
|
||||
),
|
||||
(
|
||||
"decode",
|
||||
device.decoder_utilization().ok().map(|u| u.utilization),
|
||||
),
|
||||
]
|
||||
} else {
|
||||
[("", None); 4]
|
||||
};
|
||||
sources.into_iter().flat_map(|(system, usage)| {
|
||||
Some(GpuUsage {
|
||||
system,
|
||||
usage: usage?,
|
||||
})
|
||||
})
|
||||
}
|
||||
122
src/linux/hwmon.rs
Normal file
122
src/linux/hwmon.rs
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
use std::fs::{read_dir, read_to_string, File};
|
||||
use std::io;
|
||||
use std::io::{ErrorKind, Read, Seek};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
|
||||
fn read_to_string_trimmed(path: &Path) -> io::Result<String> {
|
||||
let mut s = read_to_string(path)?;
|
||||
let len = s.trim().len();
|
||||
s.truncate(len);
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
pub struct FileSource {
|
||||
buff: String,
|
||||
file: File,
|
||||
}
|
||||
|
||||
impl FileSource {
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<FileSource> {
|
||||
Ok(FileSource {
|
||||
buff: String::with_capacity(32),
|
||||
file: File::open(path)?,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn read<T>(&mut self) -> io::Result<T>
|
||||
where
|
||||
T: FromStr,
|
||||
<T as FromStr>::Err: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
self.buff.clear();
|
||||
self.file.rewind()?;
|
||||
self.file.read_to_string(&mut self.buff)?;
|
||||
self.buff
|
||||
.trim()
|
||||
.parse()
|
||||
.map_err(|e| io::Error::new(ErrorKind::InvalidData, e))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Device {
|
||||
base_path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Device {
|
||||
pub fn new(path: PathBuf) -> io::Result<Device> {
|
||||
let name = read_to_string_trimmed(&path.join("name"))?;
|
||||
Ok(Device {
|
||||
base_path: path,
|
||||
name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list() -> impl Iterator<Item = io::Result<Device>> {
|
||||
let sensors = read_dir("/sys/class/hwmon").into_iter().flatten();
|
||||
sensors.map(|device| device.and_then(|device| Device::new(device.path())))
|
||||
}
|
||||
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn sensors(&self) -> impl Iterator<Item = io::Result<Sensor>> {
|
||||
// determine early to avoid borrowing &self in iterator
|
||||
let is_cpu_thermal = self.name == "cpu_thermal";
|
||||
|
||||
let sensors = read_dir(&self.base_path).into_iter().flatten();
|
||||
sensors
|
||||
.filter_map(|sensor| {
|
||||
let sensor = match sensor {
|
||||
Ok(sensor) => sensor,
|
||||
Err(e) => return Some(Err(e)),
|
||||
};
|
||||
|
||||
if sensor.file_name().to_str()?.ends_with("_input") {
|
||||
Some(Ok(sensor.path()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.map(move |path| {
|
||||
let path = path?;
|
||||
|
||||
let input_name = path.file_name().unwrap().to_str().unwrap();
|
||||
|
||||
// rpi cpu_thermal doesn't have labels, so we hardcode one
|
||||
if is_cpu_thermal && input_name == "temp1_input" {
|
||||
return Ok(Sensor {
|
||||
input_path: path,
|
||||
name: "Tdie".into(),
|
||||
});
|
||||
}
|
||||
|
||||
let base_name = input_name.trim_end_matches("_input");
|
||||
|
||||
let label_name = path.with_file_name(format!("{base_name}_label"));
|
||||
let name = read_to_string_trimmed(&label_name)?;
|
||||
|
||||
Ok(Sensor {
|
||||
input_path: path,
|
||||
name,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sensor {
|
||||
input_path: PathBuf,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Sensor {
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
pub fn reader(&self) -> io::Result<FileSource> {
|
||||
FileSource::open(&self.input_path)
|
||||
}
|
||||
}
|
||||
123
src/linux/mod.rs
Normal file
123
src/linux/mod.rs
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
pub mod disk;
|
||||
pub mod gpu;
|
||||
pub mod hwmon;
|
||||
pub mod power;
|
||||
pub mod sensors;
|
||||
|
||||
use self::disk::zfs::pools;
|
||||
use self::disk::*;
|
||||
use self::sensors::*;
|
||||
use crate::linux::disk::zfs::arcstats;
|
||||
use crate::linux::gpu::{update_gpu_power, utilization};
|
||||
use crate::linux::power::power_usage;
|
||||
use crate::{hostname, Error, MultiSensorSource, Result, SensorData, SensorSource};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Mutex;
|
||||
use sysconf::SysconfError;
|
||||
|
||||
impl From<SysconfError> for Error {
|
||||
fn from(_: SysconfError) -> Self {
|
||||
Error::Other("Unsupported sysconf".into())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sensors {
|
||||
pub hostname: String,
|
||||
cpu: Mutex<CpuTimeSource>,
|
||||
temp: Mutex<TemperatureSource>,
|
||||
net: Mutex<NetworkSource>,
|
||||
mem: Mutex<MemorySource>,
|
||||
disk_stats: Mutex<DiskStatSource>,
|
||||
disk_usage: Mutex<DiskUsageSource>,
|
||||
}
|
||||
|
||||
impl Sensors {
|
||||
pub fn new() -> Result<Sensors> {
|
||||
std::thread::spawn(update_gpu_power);
|
||||
|
||||
Ok(Sensors {
|
||||
hostname: hostname()?,
|
||||
cpu: Mutex::new(CpuTimeSource::new()?),
|
||||
temp: Mutex::new(TemperatureSource::new()?),
|
||||
net: Mutex::new(NetworkSource::new()?),
|
||||
mem: Mutex::new(MemorySource::new()?),
|
||||
disk_stats: Mutex::new(DiskStatSource::new()?),
|
||||
disk_usage: Mutex::new(DiskUsageSource::new()?),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_metrics(sensors: &Sensors) -> Result<String> {
|
||||
let hostname = &sensors.hostname;
|
||||
let mut disk_source = sensors.disk_stats.lock().unwrap();
|
||||
let mut disk_usage_source = sensors.disk_usage.lock().unwrap();
|
||||
let disks = disk_source.read()?;
|
||||
let disk_usage = disk_usage_source.read()?;
|
||||
let cpu = sensors.cpu.lock().unwrap().read()?;
|
||||
let memory = sensors.mem.lock().unwrap().read()?;
|
||||
let temperatures = sensors.temp.lock().unwrap().read()?;
|
||||
let mut net = sensors.net.lock().unwrap();
|
||||
let networks = net.read()?;
|
||||
let pools = pools();
|
||||
let mut result = String::with_capacity(256);
|
||||
|
||||
cpu.write(&mut result, &hostname);
|
||||
memory.write(&mut result, &hostname);
|
||||
|
||||
for pool in pools {
|
||||
writeln!(
|
||||
&mut result,
|
||||
"zfs_pool_size{{host=\"{}\", pool=\"{}\"}} {}",
|
||||
hostname, pool.name, pool.size
|
||||
)
|
||||
.ok();
|
||||
writeln!(
|
||||
&mut result,
|
||||
"zfs_pool_free{{host=\"{}\", pool=\"{}\"}} {}",
|
||||
hostname, pool.name, pool.free
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
for network in networks {
|
||||
if let Ok(network) = network {
|
||||
network.write(&mut result, &hostname);
|
||||
}
|
||||
}
|
||||
for disk in disks {
|
||||
if let Ok(disk) = disk {
|
||||
disk.write(&mut result, hostname);
|
||||
}
|
||||
}
|
||||
|
||||
for disk in disk_usage {
|
||||
if let Ok(disk) = disk {
|
||||
disk.write(&mut result, hostname);
|
||||
}
|
||||
}
|
||||
for (label, temp) in temperatures {
|
||||
if temp != 0.0 {
|
||||
writeln!(
|
||||
&mut result,
|
||||
"temperature{{host=\"{}\", sensor=\"{}\"}} {:.1}",
|
||||
hostname, label, temp
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(power) = power_usage()? {
|
||||
power.write(&mut result, &sensors.hostname);
|
||||
}
|
||||
if let Some(arc) = arcstats()? {
|
||||
arc.write(&mut result, &sensors.hostname);
|
||||
}
|
||||
if let Some(memory) = gpu::memory() {
|
||||
memory.write(&mut result, &sensors.hostname)
|
||||
}
|
||||
|
||||
for usage in utilization() {
|
||||
usage.write(&mut result, &sensors.hostname);
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
94
src/linux/power.rs
Normal file
94
src/linux/power.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
use crate::linux::gpu::gpu_power;
|
||||
use crate::{Error, Result};
|
||||
use std::fmt::Write;
|
||||
use std::fs::{read_dir, read_to_string};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use tracing::warn;
|
||||
|
||||
static CAN_READ: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct PowerUsage {
|
||||
cpu_uj: u64,
|
||||
cpu_packages_uj: Vec<u64>,
|
||||
gpu_uj: u64,
|
||||
}
|
||||
|
||||
impl PowerUsage {
|
||||
pub fn write<W: Write>(&self, mut w: W, hostname: &str) {
|
||||
writeln!(
|
||||
&mut w,
|
||||
r#"total_power{{host="{}", device="cpu"}} {:.3}"#,
|
||||
hostname,
|
||||
self.cpu_uj as f64 / 1_000_000.0
|
||||
)
|
||||
.ok();
|
||||
for (i, package) in self.cpu_packages_uj.iter().enumerate() {
|
||||
writeln!(
|
||||
&mut w,
|
||||
r#"package_power{{host="{}", package="{}", device="cpu"}} {:.3}"#,
|
||||
hostname,
|
||||
i,
|
||||
*package as f64 / 1_000_000.0
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
if self.gpu_uj > 0 {
|
||||
writeln!(
|
||||
&mut w,
|
||||
r#"total_power{{host="{}", device="gpu"}} {:.3}"#,
|
||||
hostname,
|
||||
self.gpu_uj as f64 / 1_000_000.0
|
||||
)
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn power_usage() -> Result<Option<PowerUsage>> {
|
||||
if !CAN_READ.load(Ordering::Relaxed) {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let dir = match read_dir("/sys/devices/virtual/powercap/intel-rapl") {
|
||||
Ok(dir) => dir,
|
||||
Err(_) => {
|
||||
CAN_READ.store(false, Ordering::Relaxed);
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
let mut usage = PowerUsage::default();
|
||||
for package in dir {
|
||||
let package = package?;
|
||||
if package
|
||||
.file_name()
|
||||
.to_str()
|
||||
.ok_or_else(|| Error::Other("Invalid name".into()))?
|
||||
.starts_with("intel-rapl")
|
||||
{
|
||||
let mut package_path = package.path();
|
||||
package_path.push("energy_uj");
|
||||
let package_usage = match read_to_string(&package_path) {
|
||||
Err(e) if e.raw_os_error() == Some(13) => {
|
||||
CAN_READ.store(false, Ordering::Relaxed);
|
||||
warn!(
|
||||
package_path = display(package_path.display()),
|
||||
"can\'t read power usage"
|
||||
);
|
||||
return Ok(None);
|
||||
}
|
||||
result => result,
|
||||
}?;
|
||||
let package_usage = package_usage.trim().parse::<u64>()?;
|
||||
usage.cpu_uj += package_usage;
|
||||
usage.cpu_packages_uj.push(package_usage);
|
||||
}
|
||||
}
|
||||
|
||||
usage.gpu_uj = gpu_power();
|
||||
if let Some(nvidia_power) = crate::linux::gpu::nvidia::power() {
|
||||
usage.gpu_uj = nvidia_power;
|
||||
}
|
||||
|
||||
Ok(Some(usage))
|
||||
}
|
||||
244
src/linux/sensors.rs
Normal file
244
src/linux/sensors.rs
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
use crate::data::{CpuTime, Memory, NetStats, Temperatures};
|
||||
use crate::linux::hwmon::{Device, FileSource};
|
||||
use crate::{Error, MultiSensorSource, Result, SensorSource};
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::{BufRead, BufReader, ErrorKind, Read, Seek};
|
||||
use sysconf::{sysconf, SysconfVariable};
|
||||
|
||||
pub struct TemperatureSource {
|
||||
cpu_sensors: Vec<FileSource>,
|
||||
gpu_sensors: Vec<FileSource>,
|
||||
}
|
||||
|
||||
impl TemperatureSource {
|
||||
pub fn new() -> Result<TemperatureSource> {
|
||||
let mut cpu_sensors = Vec::new();
|
||||
let mut gpu_sensors = Vec::new();
|
||||
|
||||
for device in Device::list().flatten() {
|
||||
if device.name() == "k10temp" || device.name() == "coretemp" {
|
||||
for sensor in device.sensors().flatten() {
|
||||
if sensor.name() == "Tdie" || sensor.name().starts_with("Core ") {
|
||||
cpu_sensors.push(sensor.reader()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if device.name() == "amdgpu" {
|
||||
for sensor in device.sensors().flatten() {
|
||||
if sensor.name() == "edge" {
|
||||
gpu_sensors.push(sensor.reader()?);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(TemperatureSource {
|
||||
cpu_sensors,
|
||||
gpu_sensors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn average_sensors(sensors: &mut [FileSource]) -> f32 {
|
||||
if sensors.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let mut total = 0.0;
|
||||
let mut count = 0.0;
|
||||
for sensor in sensors.iter_mut() {
|
||||
if let Ok(value) = sensor.read::<f32>() {
|
||||
total += value;
|
||||
count += 1.0
|
||||
}
|
||||
}
|
||||
total / count
|
||||
}
|
||||
|
||||
impl SensorSource for TemperatureSource {
|
||||
type Data = Temperatures;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Data> {
|
||||
let mut result = Temperatures {
|
||||
cpu: average_sensors(&mut self.cpu_sensors) / 1000.0,
|
||||
gpu: average_sensors(&mut self.gpu_sensors) / 1000.0,
|
||||
};
|
||||
|
||||
if let Some(gpu) = super::gpu::nvidia::temperature() {
|
||||
result.gpu = gpu;
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MemorySource {
|
||||
source: File,
|
||||
buff: String,
|
||||
}
|
||||
|
||||
impl MemorySource {
|
||||
pub fn new() -> Result<MemorySource> {
|
||||
Ok(MemorySource {
|
||||
source: File::open("/proc/meminfo")?,
|
||||
buff: String::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SensorSource for MemorySource {
|
||||
type Data = Memory;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Data> {
|
||||
self.buff.clear();
|
||||
self.source.rewind()?;
|
||||
self.source.read_to_string(&mut self.buff)?;
|
||||
|
||||
let mut mem = Memory::default();
|
||||
for line in self.buff.lines() {
|
||||
if let Some(line) = line.strip_suffix(" kB") {
|
||||
if let Some(line_total) = line.strip_prefix("MemTotal: ") {
|
||||
mem.total = line_total.trim().parse::<u64>()? * 1000;
|
||||
}
|
||||
if let Some(line_free) = line.strip_prefix("MemFree: ") {
|
||||
mem.free = line_free.trim().parse::<u64>()? * 1000;
|
||||
}
|
||||
if let Some(line_available) = line.strip_prefix("MemAvailable: ") {
|
||||
mem.available = line_available.trim().parse::<u64>()? * 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(mem)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CpuTimeSource {
|
||||
source: BufReader<File>,
|
||||
buff: Vec<u8>,
|
||||
cpu_count: f32,
|
||||
}
|
||||
|
||||
impl CpuTimeSource {
|
||||
pub fn new() -> Result<CpuTimeSource> {
|
||||
Ok(CpuTimeSource {
|
||||
source: BufReader::new(File::open("/proc/stat")?),
|
||||
buff: Vec::new(),
|
||||
cpu_count: sysconf(SysconfVariable::ScNprocessorsOnln)? as f32,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SensorSource for CpuTimeSource {
|
||||
type Data = CpuTime;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Data> {
|
||||
self.buff.clear();
|
||||
self.source.rewind()?;
|
||||
|
||||
self.source.read_until(b'\n', &mut self.buff)?;
|
||||
|
||||
let line = std::str::from_utf8(&self.buff)?;
|
||||
|
||||
let mut parts = line.split_ascii_whitespace();
|
||||
if let (_cpu, Some(user), _nice, Some(system)) =
|
||||
(parts.next(), parts.next(), parts.next(), parts.next())
|
||||
{
|
||||
let user: f32 = user.parse()?;
|
||||
let system: f32 = system.parse()?;
|
||||
let clock_ticks = sysconf(SysconfVariable::ScClkTck)?;
|
||||
Ok(CpuTime(
|
||||
(user + system) / (clock_ticks as f32) / self.cpu_count,
|
||||
))
|
||||
} else {
|
||||
Err(io::Error::from(ErrorKind::InvalidData).into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetworkSource {
|
||||
source: File,
|
||||
buff: String,
|
||||
}
|
||||
|
||||
impl NetworkSource {
|
||||
pub fn new() -> Result<NetworkSource> {
|
||||
Ok(NetworkSource {
|
||||
source: File::open("/proc/net/dev")?,
|
||||
buff: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_line(line: &str) -> Result<NetStats> {
|
||||
let mut parts = line.trim_start().split_ascii_whitespace();
|
||||
if let (
|
||||
Some(interface),
|
||||
Some(bytes_received),
|
||||
_packets,
|
||||
_err,
|
||||
_drop,
|
||||
_fifo,
|
||||
_frame,
|
||||
_compressed,
|
||||
_multicast,
|
||||
Some(bytes_sent),
|
||||
) = (
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
parts.next(),
|
||||
) {
|
||||
Ok(NetStats {
|
||||
interface: interface.trim_end_matches(':').into(),
|
||||
bytes_sent: bytes_sent.parse()?,
|
||||
bytes_received: bytes_received.parse()?,
|
||||
})
|
||||
} else {
|
||||
Err(Error::Io(ErrorKind::InvalidData.into()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MultiSensorSource for NetworkSource {
|
||||
type Data = NetStats;
|
||||
type Iter<'a> = NetworkStatParser<'a>;
|
||||
|
||||
fn read(&mut self) -> Result<Self::Iter<'_>> {
|
||||
self.buff.clear();
|
||||
self.source.rewind()?;
|
||||
self.source.read_to_string(&mut self.buff)?;
|
||||
|
||||
Ok(NetworkStatParser {
|
||||
lines: self.buff.lines(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NetworkStatParser<'a> {
|
||||
lines: std::str::Lines<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Iterator for NetworkStatParser<'a> {
|
||||
type Item = Result<NetStats>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let line = loop {
|
||||
let line = self.lines.next()?;
|
||||
let trimmed = line.trim_start();
|
||||
if trimmed.starts_with("en") || trimmed.starts_with("eth") || trimmed.starts_with("wlp")
|
||||
{
|
||||
break trimmed;
|
||||
}
|
||||
};
|
||||
|
||||
Some(NetworkSource::parse_line(line))
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue