librehardwaremonitor integration

This commit is contained in:
Robin Appelman 2023-05-18 18:40:10 +02:00
commit bfb8f20439
8 changed files with 358 additions and 62 deletions

View file

@ -182,3 +182,41 @@ impl SensorData for DiskUsage {
}
}
}
#[derive(Debug, Default)]
pub struct PowerUsage {
pub cpu_uj: u64,
pub cpu_packages_uj: Vec<u64>,
pub 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();
}
}
}

View file

@ -1,3 +1,4 @@
use crate::data::PowerUsage;
use crate::linux::gpu::gpu_power;
use crate::{Error, Result};
use std::fmt::Write;
@ -7,44 +8,6 @@ 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);

View file

@ -1,4 +1,5 @@
use bollard::Docker;
use clap::Parser;
use color_eyre::{Report, Result};
use futures_util::pin_mut;
use futures_util::StreamExt;
@ -26,6 +27,14 @@ impl From<Report> for ReportRejection {
impl Reject for ReportRejection {}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// Port to listen to
#[arg(short, long)]
port: Option<u16>,
}
async fn serve_inner(docker: Option<Docker>, sensors: &Sensors) -> Result<String> {
let mut metrics = get_metrics(sensors)?;
if let Some(docker) = docker {
@ -50,12 +59,16 @@ async fn serve_metrics(docker: Option<Docker>, sensors: Arc<Sensors>) -> Result<
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::fmt::init();
let args = Args::parse();
let host_port: u16 = dotenvy::var("PORT")
.ok()
.map(|port| port.parse())
.transpose()?
.unwrap_or(80);
let host_port = match args.port {
Some(port) => port,
None => dotenvy::var("PORT")
.ok()
.map(|port| port.parse())
.transpose()?
.unwrap_or(80),
};
let mdns = dotenvy::var("DISABLE_MDNS").is_ok();

View file

@ -12,6 +12,7 @@ use once_cell::sync::Lazy;
use os_thread_local::ThreadLocal;
use std::borrow::Cow;
use std::sync::Mutex;
use std::thread::spawn;
use sysinfo::{ComponentExt, DiskExt, NetworkExt, System, SystemExt};
pub struct Sensors {
@ -26,6 +27,7 @@ static WMI: Lazy<ThreadLocal<WmiSensor>> =
impl Sensors {
pub fn new() -> Result<Sensors> {
spawn(wmi::update_power);
let mut system = System::new_all();
system.refresh_all();
println!("{:?}", system);
@ -96,6 +98,9 @@ pub fn get_metrics(sensors: &Sensors) -> Result<String> {
if let Some(disk_usage) = WMI.with(|wmi| wmi.disk_usage())? {
disk_usage.write(&mut result, &hostname);
}
let hwmon_data = WMI.with(|wmi| wmi.hwmon())?;
hwmon_data.temperature.write(&mut result, &hostname);
hwmon_data.power.write(&mut result, &hostname);
Ok(result)
}

View file

@ -1,46 +1,54 @@
use crate::data::DiskStats;
use crate::data::{DiskStats, PowerUsage, Temperatures};
use crate::Result;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Mutex;
use std::thread::sleep;
use std::time::{Duration, Instant};
use wmi::{COMLibrary, WMIConnection};
pub struct WmiSensor {
wmi_con: WMIConnection,
wmi_hwmon_con: Option<WMIConnection>,
}
impl WmiSensor {
pub fn new() -> Result<Self> {
let com_con = COMLibrary::new()?;
let wmi_con = WMIConnection::new(com_con.into())?;
let wmi_con = WMIConnection::new(com_con)?;
let wmi_hwmon_con =
WMIConnection::with_namespace_path("ROOT\\LibreHardwareMonitor", com_con).ok();
Ok(WmiSensor { wmi_con })
Ok(WmiSensor {
wmi_con,
wmi_hwmon_con,
})
}
pub fn gpu_mem(&self) -> Result<u64> {
#[derive(Deserialize, Debug)]
#[allow(non_camel_case_types)]
struct Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory {
#[serde(rename = "Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory")]
struct GPUAdapterMemory {
#[serde(rename = "DedicatedUsage")]
dedicated_usage: u64,
}
let results: Vec<Win32_PerfFormattedData_GPUPerformanceCounters_GPUAdapterMemory> =
self.wmi_con.query()?;
let results: Vec<GPUAdapterMemory> = self.wmi_con.query()?;
Ok(results.iter().map(|result| result.dedicated_usage).sum())
}
pub fn gpu_usage(&self) -> Result<HashMap<String, u32>> {
#[derive(Deserialize, Debug)]
#[allow(non_camel_case_types)]
struct Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine {
#[serde(rename = "Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine")]
struct GPUEngine {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "UtilizationPercentage")]
usage: u32,
}
let results: Vec<Win32_PerfFormattedData_GPUPerformanceCounters_GPUEngine> =
self.wmi_con.query()?;
let results: Vec<GPUEngine> = self.wmi_con.query()?;
let mut data = HashMap::default();
@ -56,8 +64,8 @@ impl WmiSensor {
pub fn disk_usage(&self) -> Result<Option<DiskStats>> {
#[derive(Deserialize, Debug)]
#[allow(non_camel_case_types)]
struct Win32_PerfRawData_Counters_FileSystemDiskActivity {
#[serde(rename = "Win32_PerfRawData_Counters_FileSystemDiskActivity")]
struct FileSystemDiskActivity {
#[serde(rename = "Name")]
name: String,
#[serde(rename = "FileSystemBytesRead")]
@ -66,8 +74,7 @@ impl WmiSensor {
written: u64,
}
let results: Vec<Win32_PerfRawData_Counters_FileSystemDiskActivity> =
self.wmi_con.query()?;
let results: Vec<FileSystemDiskActivity> = self.wmi_con.query()?;
for result in results {
if result.name == "_Total" {
return Ok(Some(DiskStats {
@ -79,4 +86,99 @@ impl WmiSensor {
}
Ok(None)
}
pub fn hwmon(&self) -> Result<HwMonData> {
let sensors: Vec<Sensor> = match self.wmi_hwmon_con.as_ref() {
Some(wmi) => wmi.query()?,
None => Vec::default(),
};
let temperature = Temperatures {
cpu: avg_sensors(&sensors, |sensor| {
sensor.sensor_type == "Temperature"
&& sensor.name.starts_with("CPU Core")
&& !sensor.name.contains("Distance")
}),
gpu: avg_sensors(&sensors, |sensor| {
sensor.sensor_type == "Temperature" && sensor.name == "GPU Core"
}),
};
Ok(HwMonData {
temperature,
power: power(),
})
}
}
#[derive(Deserialize, Debug)]
#[serde(rename_all = "PascalCase")]
#[allow(dead_code)]
struct Sensor {
identifier: String,
name: String,
sensor_type: String,
value: f32,
}
fn avg_sensors(sensors: &[Sensor], filter: impl Fn(&Sensor) -> bool) -> f32 {
let count = sensors.iter().filter(|sensor| filter(*sensor)).count();
let total: f32 = sensors
.iter()
.filter_map(|sensor| filter(sensor).then_some(sensor.value))
.sum();
total / count as f32
}
pub struct HwMonData {
pub temperature: Temperatures,
pub power: PowerUsage,
}
static CPU_POWER_UJ: AtomicU64 = AtomicU64::new(0);
static GPU_POWER_UJ: AtomicU64 = AtomicU64::new(0);
static POWER_LAST_READ: Mutex<Option<Instant>> = Mutex::new(None);
fn get_power_elapsed() -> Option<Duration> {
let mut last_read = 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
}
fn get_sensor(sensors: &[Sensor], ty: &str, name: &str) -> Option<f32> {
sensors.iter().find_map(|sensor| {
(sensor.sensor_type == ty && sensor.name == name).then_some(sensor.value)
})
}
pub fn update_power() {
let Ok(com_con) = COMLibrary::new() else {return;};
if let Ok(wmi_con) = WMIConnection::with_namespace_path("ROOT\\LibreHardwareMonitor", com_con) {
loop {
if let Some(elapsed) = get_power_elapsed() {
let Ok(sensors) = wmi_con.query::<Sensor>() else {return;};
let sensors: Vec<Sensor> = sensors;
let Some(cpu_current_power) = get_sensor(&sensors, "Power", "CPU Package") else {return;};
let Some(gpu_current_power) = get_sensor(&sensors, "Power", "GPU Package") else {return;};
let elapsed_sec = elapsed.as_secs_f32();
let cpu_power = cpu_current_power * elapsed_sec * 1_000_000.0;
let gpu_power = gpu_current_power * elapsed_sec * 1_000_000.0;
CPU_POWER_UJ.fetch_add(cpu_power as u64, Ordering::SeqCst);
GPU_POWER_UJ.fetch_add(gpu_power as u64, Ordering::SeqCst);
}
sleep(Duration::from_millis(500));
}
}
}
pub fn power() -> PowerUsage {
PowerUsage {
cpu_uj: CPU_POWER_UJ.load(Ordering::SeqCst),
cpu_packages_uj: Vec::default(),
gpu_uj: GPU_POWER_UJ.load(Ordering::SeqCst),
}
}