#![allow(unsafe_code)]
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use crate::collector::error::{CollectError, CollectErrorKind};
pub trait FileSource: Send + Sync + std::fmt::Debug {
fn read_to_string(&self, path: &Path) -> Result<String, CollectError>;
fn available_parallelism(&self) -> Option<usize>;
fn statvfs(&self, path: &Path) -> Result<RawStatvfs, CollectError>;
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any>
where
Self: 'static,
{
None
}
}
#[derive(Clone, Debug)]
pub struct ProcSource {
inner: Arc<dyn FileSource>,
os_release_override: Option<PathBuf>,
logical_cores: Option<usize>,
stat_path: PathBuf,
loadavg_path: PathBuf,
meminfo_path: PathBuf,
}
impl ProcSource {
#[must_use]
pub fn production() -> Self {
Self {
inner: Arc::new(HostSource),
os_release_override: None,
logical_cores: None,
stat_path: PathBuf::from("/proc/stat"),
loadavg_path: PathBuf::from("/proc/loadavg"),
meminfo_path: PathBuf::from("/proc/meminfo"),
}
}
#[must_use]
pub fn inner(&self) -> &Arc<dyn FileSource> {
&self.inner
}
#[must_use]
pub fn for_source(inner: Arc<dyn FileSource>) -> Self {
Self {
inner,
os_release_override: None,
logical_cores: None,
stat_path: PathBuf::from("/proc/stat"),
loadavg_path: PathBuf::from("/proc/loadavg"),
meminfo_path: PathBuf::from("/proc/meminfo"),
}
}
#[must_use]
pub fn for_memory(inner: MemorySource) -> Self {
Self::for_source(Arc::new(inner))
}
#[must_use]
pub fn memory_source_mut(&mut self) -> Option<&mut MemorySource> {
let arc = Arc::get_mut(&mut self.inner)?;
arc.as_any_mut()?.downcast_mut::<MemorySource>()
}
#[must_use]
pub fn with_os_release_path(mut self, path: impl Into<PathBuf>) -> Self {
self.os_release_override = Some(path.into());
self
}
#[must_use]
pub fn with_logical_cores(mut self, cores: usize) -> Self {
self.logical_cores = Some(cores);
self
}
#[must_use]
pub fn with_stat_path(mut self, path: impl Into<PathBuf>) -> Self {
self.stat_path = path.into();
self
}
#[must_use]
pub fn with_loadavg_path(mut self, path: impl Into<PathBuf>) -> Self {
self.loadavg_path = path.into();
self
}
#[must_use]
pub fn with_meminfo_path(mut self, path: impl Into<PathBuf>) -> Self {
self.meminfo_path = path.into();
self
}
pub fn read_proc_stat(&self) -> Result<ParsedProcStat, CollectError> {
let raw = self.read_path(&self.stat_path)?;
cpu::parse_proc_stat(&raw)
}
pub fn read_proc_loadavg(&self) -> Result<String, CollectError> {
self.read_path(&self.loadavg_path)
}
pub fn read_proc_meminfo(&self) -> Result<ParsedMeminfo, CollectError> {
let raw = self.read_path(&self.meminfo_path)?;
memory::parse_meminfo(&raw)
}
pub fn read_mountinfo(&self) -> Result<String, CollectError> {
self.read_path(Path::new("/proc/self/mountinfo"))
}
pub fn statvfs(&self, path: &Path) -> Result<RawStatvfs, CollectError> {
self.inner.statvfs(path)
}
pub fn read_os_release(&self) -> Result<Option<String>, CollectError> {
let path = self
.os_release_override
.clone()
.unwrap_or_else(|| PathBuf::from("/etc/os-release"));
match self.inner.read_to_string(&path) {
Ok(s) => Ok(Some(s)),
Err(err) if err.kind == CollectErrorKind::SourceUnavailable => Ok(None),
Err(err) => Err(err),
}
}
pub fn kernel_identity(&self) -> Result<KernelIdentity, CollectError> {
let sysname = self
.read_optional("/proc/sys/kernel/ostype")?
.unwrap_or_else(|| "Linux".to_string());
let release = self
.read_optional("/proc/sys/kernel/osrelease")?
.unwrap_or_else(|| "unknown".to_string());
Ok(KernelIdentity { sysname, release })
}
pub fn architecture(&self) -> String {
if let Ok(Some(arch)) = self.read_optional("/proc/sys/kernel/arch") {
return arch.trim().to_string();
}
if let Ok(raw) = self.read_path(Path::new("/proc/cpuinfo")) {
for line in raw.lines() {
if let Some(rest) = line.strip_prefix("machine") {
let value = rest.trim_start_matches(|c: char| c == ':' || c.is_whitespace());
if !value.is_empty() {
return value.to_string();
}
}
}
}
"unknown".to_string()
}
pub fn hostname(&self) -> Result<String, CollectError> {
let raw = self
.read_path(Path::new("/proc/sys/kernel/hostname"))?
.trim()
.to_string();
if raw.is_empty() {
return Err(CollectError::new(
CollectErrorKind::SourceUnavailable,
"hostname from /proc/sys/kernel/hostname was empty",
));
}
Ok(raw)
}
#[must_use]
pub fn logical_core_count(&self) -> Option<usize> {
self.logical_cores
.or_else(|| self.inner.available_parallelism())
}
fn read_path(&self, path: &Path) -> Result<String, CollectError> {
self.inner.read_to_string(path)
}
fn read_optional(&self, path: &str) -> Result<Option<String>, CollectError> {
match self.inner.read_to_string(Path::new(path)) {
Ok(s) => Ok(Some(s)),
Err(err) if err.kind == CollectErrorKind::SourceUnavailable => Ok(None),
Err(err) => Err(err),
}
}
}
#[derive(Debug)]
struct HostSource;
impl FileSource for HostSource {
fn read_to_string(&self, path: &Path) -> Result<String, CollectError> {
match fs::read_to_string(path) {
Ok(s) => Ok(s),
Err(err) => Err(map_io_error(path, err)),
}
}
fn available_parallelism(&self) -> Option<usize> {
std::thread::available_parallelism()
.ok()
.map(std::num::NonZeroUsize::get)
}
fn statvfs(&self, path: &Path) -> Result<RawStatvfs, CollectError> {
let c_path = std::ffi::CString::new(path.as_os_str().as_encoded_bytes())
.map_err(|_| CollectError::new(CollectErrorKind::Parse, "mount path contains NUL"))?;
let mut stat = std::mem::MaybeUninit::<libc::statvfs>::uninit();
let result = unsafe { libc::statvfs(c_path.as_ptr(), stat.as_mut_ptr()) };
if result != 0 {
return Err(CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("statvfs failed for {}", path.display()),
));
}
let stat = unsafe { stat.assume_init() };
Ok(RawStatvfs {
blocks: stat.f_blocks,
free_blocks: stat.f_bfree,
available_blocks: stat.f_bavail,
fragment_size: stat.f_frsize,
block_size: stat.f_bsize,
})
}
}
fn map_io_error(path: &Path, err: io::Error) -> CollectError {
use io::ErrorKind;
let path_display = path.display().to_string();
match err.kind() {
ErrorKind::NotFound => CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("source file not found: {path_display}"),
)
.with_source(err),
ErrorKind::PermissionDenied => CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("source file permission denied: {path_display}"),
)
.with_source(err),
_ => CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("source read error: {err}"),
)
.with_source(err),
}
}
#[derive(Debug, Clone, Default)]
pub struct MemorySource {
files: std::collections::HashMap<PathBuf, String>,
stats: std::collections::HashMap<PathBuf, RawStatvfs>,
logical_cores: Option<usize>,
}
impl MemorySource {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_file(mut self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
self.files.insert(path.into(), content.into());
self
}
pub fn add_file(&mut self, path: impl Into<PathBuf>, content: impl Into<String>) {
self.files.insert(path.into(), content.into());
}
pub fn add_statvfs(&mut self, path: impl Into<PathBuf>, stats: RawStatvfs) {
self.stats.insert(path.into(), stats);
}
#[must_use]
pub fn has_file(&self, path: &str) -> bool {
self.files.contains_key(Path::new(path))
}
#[must_use]
pub fn with_logical_cores(mut self, cores: usize) -> Self {
self.logical_cores = Some(cores);
self
}
}
impl FileSource for MemorySource {
fn read_to_string(&self, path: &Path) -> Result<String, CollectError> {
if let Some(content) = self.files.get(path) {
Ok(content.clone())
} else {
let display = path.display().to_string();
Err(CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("fixture missing: {display}"),
))
}
}
fn available_parallelism(&self) -> Option<usize> {
self.logical_cores
}
fn statvfs(&self, path: &Path) -> Result<RawStatvfs, CollectError> {
self.stats.get(path).copied().ok_or_else(|| {
CollectError::new(
CollectErrorKind::SourceUnavailable,
format!("fixture statvfs missing: {}", path.display()),
)
})
}
fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any>
where
Self: 'static,
{
Some(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RawStatvfs {
pub blocks: u64,
pub free_blocks: u64,
pub available_blocks: u64,
pub fragment_size: u64,
pub block_size: u64,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ParsedProcStat {
pub aggregate: Option<crate::collector::linux::CpuCounters>,
}
#[derive(Debug, Default, Clone, PartialEq)]
pub struct ParsedMeminfo {
pub mem_total_kb: Option<u64>,
pub mem_available_kb: Option<u64>,
pub mem_free_kb: Option<u64>,
pub buffers_kb: Option<u64>,
pub cached_kb: Option<u64>,
pub s_reclaimable_kb: Option<u64>,
pub swap_total_kb: Option<u64>,
pub swap_free_kb: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct KernelIdentity {
pub sysname: String,
pub release: String,
}
use crate::collector::linux::{cpu, memory};