feat: add telemetry subcrate #15

Open
nici wants to merge 3 commits from nici/zenyx-engine-telemetry:telemetry into main
30 changed files with 1838 additions and 124 deletions

572
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -15,7 +15,7 @@ crate-type = ["cdylib"]
[workspace]
resolver = "2"
members = ["subcrates/renderer", "subcrates/zlog"]
members = ["subcrates/renderer", "subcrates/telemetry", "subcrates/zlog"]
[workspace.dependencies]
zlog = { path = "subcrates/zlog" }
@ -55,6 +55,8 @@ vulkano = "0.35.1"
wgpu = { version = "25.0.0", features = ["spirv"] }
zlog.workspace = true
allocator-api2 = "0.2.21"
serde_json = "1.0.140"
serde = "1.0.219"
[target.aarch64-linux-android.dependencies]
winit = { version = "0.30.9", features = ["android-native-activity"] }

View file

@ -236,7 +236,10 @@ mod tests {
assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 2).unwrap(), 3);
assert_eq!(sparse_set.sparse[1].as_ref().unwrap().1, 2);
assert_eq!(sparse_set.keys(), [10, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]);
assert_eq!(
sparse_set.keys(),
[10, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]
);
assert_eq!(sparse_set.values(), [1, 2, 2, 1, 2]);
assert_eq!(sparse_set.remove(SPARSE_PAGESIZE + 1).unwrap(), 2);
@ -249,25 +252,44 @@ mod tests {
assert_eq!(sparse_set.keys(), [10, 11, 12]);
assert_eq!(sparse_set.values(), [1, 2, 2]);
sparse_set.insert(SPARSE_PAGESIZE, 1);
sparse_set.insert(SPARSE_PAGESIZE + 1, 2);
sparse_set.insert(SPARSE_PAGESIZE + 2, 3);
assert_eq!(sparse_set.remove(10).unwrap(), 1);
assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 2);
// swap-remove
assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, 11, 12, SPARSE_PAGESIZE, SPARSE_PAGESIZE + 1]);
// swap-remove
assert_eq!(
sparse_set.keys(),
[
SPARSE_PAGESIZE + 2,
11,
12,
SPARSE_PAGESIZE,
SPARSE_PAGESIZE + 1
]
);
assert_eq!(sparse_set.values(), [3, 2, 2, 1, 2]);
assert_eq!(sparse_set.remove(11).unwrap(), 2);
assert_eq!(sparse_set.sparse[0].as_ref().unwrap().1, 1);
assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, 12, SPARSE_PAGESIZE]);
assert_eq!(
sparse_set.keys(),
[
SPARSE_PAGESIZE + 2,
SPARSE_PAGESIZE + 1,
12,
SPARSE_PAGESIZE
]
);
assert_eq!(sparse_set.values(), [3, 2, 2, 1]);
assert_eq!(sparse_set.remove(12).unwrap(), 2);
assert!(sparse_set.sparse[0].is_none());
assert_eq!(sparse_set.keys(), [SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, SPARSE_PAGESIZE]);
assert_eq!(
sparse_set.keys(),
[SPARSE_PAGESIZE + 2, SPARSE_PAGESIZE + 1, SPARSE_PAGESIZE]
);
assert_eq!(sparse_set.values(), [3, 2, 1]);
}
}

View file

@ -0,0 +1,27 @@
[package]
name = "telemetry"
version = "0.1.0"
edition = "2024"
[dependencies]
ash = "0.38.0"
chrono = { version = "0.4.40", features = ["serde"] }
derive_more = { version = "2.0.1", features = ["add", "add_assign", "as_ref", "deref_mut", "from", "from_str", "full", "into", "mul", "mul_assign", "not", "sum", "try_from", "try_into", "try_unwrap", "unwrap"] }
detect-desktop-environment = "1.2.0"
hidapi = "2.6.3"
humansize = "2.1.3"
metal = { version = "0.32.0", optional = true }
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
strum = "0.27.1"
strum_macros = "0.27.1"
sys-locale = "0.3.2"
sysinfo = { version = "0.34.2", features = ["serde"] }
thiserror = "2.0.12"
timeago = "0.4.2"
versions = { version = "7.0.0", features = ["serde"] }
wgpu = "25.0.0"
winit = "0.30.9"
[features]
metal = ["dep:metal"]

View file

@ -0,0 +1,21 @@
use std::{env, fs::File, io::Write, process::Command};
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let mut f = File::create(format!("{}/built.rs", out_dir)).unwrap();
let commit_hash = Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
.ok()
.and_then(|o| String::from_utf8(o.stdout).ok())
.unwrap_or_else(|| "unknown".into());
writeln!(f, "#[allow(dead_code)]").unwrap();
writeln!(
f,
"pub const GIT_COMMIT_HASH: &str = \"{}\";",
commit_hash.trim()
)
.unwrap();
}

View file

@ -0,0 +1,70 @@
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
#[macro_use]
mod modules;
pub use crate::modules::{cpu, devices, external, gpu, memory, meta, network, os, storage, uptime};
use thiserror::Error;
#[derive(Debug, Serialize, Deserialize)]
pub struct TelemetryInfo {
telemetry: AllInfo,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AllInfo {
gpu: gpu::GPUInfo,
cpu: Vec<cpu::CPUInfo>,
memory: memory::MemoryInfo,
os: os::OSInfo,
storage: Vec<storage::StorageInfo>,
network: network::NetworkInfo,
external: external::ExternalInfo,
devices: Vec<devices::DeviceInfo>,
meta: meta::MetaInfo,
uptime: uptime::UptimeInfo,
}
#[derive(Debug, Error)]
pub enum TelemetryError {
// #[error("Failed to get GPU information")]
// GPUError(#[from] gpu::GPUError),
#[error("Failed to get CPU information")]
CPUError(#[from] cpu::CPUError),
#[error("Failed to get OS information")]
OSError(#[from] os::OSError),
#[error("Failed to get storage information")]
StorageError(#[from] storage::StorageError),
// #[error("Failed to get external information")]
// ExternalError(#[from] external::ExternalError),
#[error("Failed to get devices information")]

Nit, this could be a float instead.

Nit, this could be a float instead.

Or even an int...

Or even an int...

I suggested a float as some monitors run at strange refresh rates, such as 59.97 hz, so a float would be the only way to store that as a non-string value.

I suggested a float as some monitors run at *strange* refresh rates, such as 59.97 hz, so a float would be the only way to store that as a non-string value.

ah fair

ah fair
DevicesError(#[from] devices::DevicesError),
#[error("Failed to get meta information")]
MetaError(#[from] meta::MetaError),
#[error("Failed to get uptime information")]
UptimeError(#[from] uptime::UptimeError),
#[error("Failed to build JSON for TelemetryInfo")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<TelemetryInfo, TelemetryError> {
Ok(TelemetryInfo {
telemetry: AllInfo {
gpu: gpu::get_struct(),
cpu: cpu::get_struct()?,
memory: memory::get_struct(),
os: os::get_struct()?,
storage: storage::get_list()?,
network: network::get_struct(),
external: external::get_struct(),
devices: devices::get_struct()?,
meta: meta::get_struct()?,
uptime: uptime::get_struct()?,
},
})
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, TelemetryError> {
Ok(json!(get_struct()?))
}

View file

@ -0,0 +1,13 @@
use serde_json;

This file should not be in the subcrate.

This file should not be in the subcrate.
fn main() {
//println!("{:#?}", telemetry::get_struct());
//println!("{:#?}", telemetry::os::get_json());
//telemetry::os::get_json();
match telemetry::get_struct() {
//Ok(telemetry) => println!("{:#?}", serde_json::from_value(telemetry)),
Ok(telemetry) => println!("{:#?}", telemetry),
Err(e) => eprintln!("Error: {}", e),
}
}

View file

@ -0,0 +1,67 @@
use crate::modules::{FmtCores, FmtThreads};
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use std::collections::HashMap;
use sysinfo::{CpuRefreshKind, RefreshKind, System};
use thiserror::Error;
#[derive(Debug, Serialize, Deserialize)]
pub struct CPUInfo {
vendor: String,
brand: String,
total_system_cores: FmtCores,
threads: FmtThreads,
architecture: String,
byte_order: String,
}
#[derive(Debug, Error)]
pub enum CPUError {
#[error("No CPU information available")]
NoCpusFound,
#[error("Failed to build JSON for Vec<CPUInfo>")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<Vec<CPUInfo>, CPUError> {
let mut system =
System::new_with_specifics(RefreshKind::nothing().with_cpu(CpuRefreshKind::everything()));
system.refresh_cpu_all();
let mut processor_map: HashMap<String, CPUInfo> = HashMap::new();
for cpu in system.cpus() {
let brand = cpu.brand().trim().to_string();
let entry = processor_map
.entry(brand.clone())
.or_insert_with(|| CPUInfo {
vendor: cpu.vendor_id().trim().to_string(),
brand: brand.clone(),
total_system_cores: System::physical_core_count()
.map(|cores| cores.into())
.unwrap_or_else(|| 0.into()),
threads: 0.into(),
architecture: System::cpu_arch().trim().to_string(),
byte_order: if cfg!(target_endian = "little") {
String::from("little-endian")
} else {
String::from("big-endian")
},
});
entry.threads += 1.into();
}
let cpus: Vec<_> = processor_map.into_values().collect();
if cpus.is_empty() {
return Err(CPUError::NoCpusFound);
}
Ok(cpus)
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, CPUError> {
Ok(json!(get_struct()?))
}

View file

@ -0,0 +1,59 @@
use super::UNKNOWN;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use thiserror::Error;
extern crate hidapi;
use hidapi::{HidApi, HidError};
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize)]
pub struct DeviceInfo {
manufacturer: String,
products: Vec<String>,
}
#[derive(Debug, Error)]
pub enum DevicesError {
#[error("Failed to initialize HID API: {0}")]
HidApiInitError(#[from] HidError),
#[error("Failed to build JSON for Vec<DeviceInfo>")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<Vec<DeviceInfo>, DevicesError> {
let api = HidApi::new()?;
let mut grouped: HashMap<String, Vec<String>> = HashMap::new();
for device in api.device_list() {
let manufacturer = device
.manufacturer_string()
.unwrap_or(UNKNOWN)
.to_ascii_lowercase();
let product = device.product_string().unwrap_or(UNKNOWN).to_string();
if !manufacturer.trim().is_empty() && !product.trim().is_empty() {
grouped.entry(manufacturer).or_default().push(product)
}
}
let mut sorted_devices: Vec<_> = grouped.into_iter().collect();
sorted_devices.sort_by(|a, b| a.0.cmp(&b.0));
Ok(sorted_devices
.into_iter()
.map(|(manufacturer, mut products)| {
products.sort(); // optional: sort product names
products.dedup(); // remove duplicates
DeviceInfo {
manufacturer,
products,
}
})
.collect())
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, DevicesError> {
Ok(json!(get_struct()?))
}

View file

@ -0,0 +1,63 @@
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use std::{cmp::Ordering::Equal, collections::HashMap};
use sysinfo::{System, Users};
#[derive(Debug, Serialize, Deserialize)]
pub struct SoftwareInfo {
name: String,
count: usize,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ExternalInfo {
softwares: Vec<SoftwareInfo>,
}
#[allow(dead_code)]
pub fn get_struct() -> ExternalInfo {
let mut system = System::new_all();
system.refresh_all();
let users = &Users::new_with_refreshed_list();
let mut grouped_processes: HashMap<String, Vec<i32>> = HashMap::new();
for process in system.processes().values() {
let name = process.name().to_str().unwrap().to_string();
let user_id = process.user_id();
let is_user_owned = user_id.is_some_and(|uid| {
users
.list()
.iter()
.any(|u| u.id() == uid && u.name() != "root")
});
if is_user_owned && !name.trim().is_empty() && !name.starts_with('[') {
grouped_processes
.entry(name)
.or_default()
.push(process.pid().as_u32() as i32);
}
}
let mut softwares = Vec::new();
let mut grouped_vec: Vec<_> = grouped_processes.into_iter().collect();
grouped_vec.sort_by(|a, b| match b.1.len().cmp(&a.1.len()) {
Equal => a.0.to_lowercase().cmp(&b.0.to_lowercase()),
other => other,
});
for (name, pids) in grouped_vec {
softwares.push(SoftwareInfo {
name,
count: pids.len(),
});
}
ExternalInfo { softwares }
}
#[allow(dead_code)]
pub fn get_json() -> Value {
json!(get_struct())
}

View file

@ -0,0 +1,69 @@
use serde::{Serialize, Serializer, Deserialize, Deserializer, de::Error as DeError};
use std::{
fmt::{Debug, Display, Formatter, Result as FmtResult},
ops::Deref,
};
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Serialize, Deserialize)]
pub enum TargetPointerWidth {
W32Bit,
W64Bit,
}
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct FmtOSArchitecture(pub TargetPointerWidth);
impl FmtOSArchitecture {
pub const W32_BIT: Self = FmtOSArchitecture(TargetPointerWidth::W32Bit);
pub const W64_BIT: Self = FmtOSArchitecture(TargetPointerWidth::W64Bit);
}
impl Display for FmtOSArchitecture {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(
f,
"{}",
match self.0 {
TargetPointerWidth::W32Bit => "32 Bit",
TargetPointerWidth::W64Bit => "64 Bit",
}
)
}
}
impl Debug for FmtOSArchitecture {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self, f)
}
}
impl Serialize for FmtOSArchitecture {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for FmtOSArchitecture {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
match s.as_str() {
"32 Bit" => Ok(FmtOSArchitecture(TargetPointerWidth::W32Bit)),
"64 Bit" => Ok(FmtOSArchitecture(TargetPointerWidth::W64Bit)),
_ => Err(DeError::custom(format!("Invalid architecture: {}", s))),
}
}
}
impl Deref for FmtOSArchitecture {
type Target = TargetPointerWidth;
fn deref(&self) -> &Self::Target {
&self.0
}
}

View file

@ -0,0 +1,144 @@
use chrono::{DateTime, Local, TimeZone, Utc};
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use std::{
fmt::{Debug, Display, Formatter, Result as FMTResult},
ops::Deref,
};
#[derive(Debug, Serialize, Deserialize)]
pub struct DateTimeInfo {
date: String,
time: String,
}
#[derive(Clone)]
pub struct FmtDateTime<Tz: TimeZone>(pub DateTime<Tz>);
impl<Tz: TimeZone> Display for FmtDateTime<Tz>
where
Tz: TimeZone,
Tz::Offset: std::fmt::Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FMTResult {
write!(
f,
"{:#?}",
DateTimeInfo {
date: self.0.format("%Y-%m-%d").to_string(),
time: self.0.format("%H:%M:%S").to_string()
}
)
}
}
impl<Tz: TimeZone> Debug for FmtDateTime<Tz>
where
Tz::Offset: std::fmt::Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FMTResult {
Display::fmt(self, f)
}
}
impl<Tz: TimeZone> Serialize for FmtDateTime<Tz>
where
DateTime<Tz>: Display,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de, Tz> Deserialize<'de> for FmtDateTime<Tz>
where
Tz: TimeZone,
DateTime<Tz>: ToString + std::str::FromStr,
<DateTime<Tz> as std::str::FromStr>::Err: std::fmt::Display,
{
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// 1. Pull a String out of the deserializer
let s = String::deserialize(deserializer)?;
// 2. Parse it, mapping any parse-error into a Serde error
<DateTime<Tz> as std::str::FromStr>::from_str(&s) // explicit, no ambiguity
.map(FmtDateTime)
.map_err(serde::de::Error::custom)
}
}
impl<Tz: TimeZone> Deref for FmtDateTime<Tz> {
type Target = DateTime<Tz>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<Tz: TimeZone> Eq for FmtDateTime<Tz> where DateTime<Tz>: Eq {}
impl<Tz: TimeZone> PartialEq for FmtDateTime<Tz>
where
DateTime<Tz>: PartialEq,
{
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl<Tz: TimeZone> PartialOrd for FmtDateTime<Tz>
where
DateTime<Tz>: PartialOrd,
{
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<Tz: TimeZone> Ord for FmtDateTime<Tz>
where
DateTime<Tz>: Ord,
{
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.0.cmp(&other.0)
}
}
impl From<FmtDateTime<Utc>> for FmtDateTime<Local> {
fn from(dt: FmtDateTime<Utc>) -> Self {
FmtDateTime(dt.0.with_timezone(&Local))
}
}
impl From<FmtDateTime<Local>> for FmtDateTime<Utc> {
fn from(dt: FmtDateTime<Local>) -> Self {
FmtDateTime(dt.0.with_timezone(&Utc))
}
}
#[allow(dead_code)]
pub trait IntoDateTime {
fn into_utc(self) -> FmtDateTime<Utc>;
fn into_local(self) -> FmtDateTime<Local>;
}
impl IntoDateTime for FmtDateTime<Local> {
fn into_utc(self) -> FmtDateTime<Utc> {
FmtDateTime(self.0.with_timezone(&Utc))
}
fn into_local(self) -> FmtDateTime<Local> {
self
}
}
impl IntoDateTime for FmtDateTime<Utc> {
fn into_local(self) -> FmtDateTime<Local> {
FmtDateTime(self.0.with_timezone(&Local))
}
fn into_utc(self) -> FmtDateTime<Utc> {
self
}
}

View file

@ -0,0 +1,44 @@
use std::str::FromStr;
use serde::{Serialize, Deserialize};
use detect_desktop_environment::DesktopEnvironment;
use strum_macros::EnumString;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, EnumString)]
#[strum(serialize_all = "PascalCase", ascii_case_insensitive)]
#[non_exhaustive]
pub enum FmtDE {
Cinnamon,
Cosmic,
CosmicEpoch,
Dde,
Ede,
Endless,
Enlightenment,
Gnome,
Hyprland,
Kde,
Lxde,
Lxqt,
MacOs,
Mate,
Old,
Pantheon,
Razor,
Rox,
Sway,
Tde,
Unity,
Windows,
Xfce,
}
impl FmtDE {
pub fn detect() -> Option<Self> {
DesktopEnvironment::detect().and_then(|inner_de| {
let s = format!("{:?}", inner_de);
FmtDE::from_str(&s).ok()
})
}
}

View file

@ -0,0 +1,7 @@
pub mod architecture;
pub mod date_time;
pub mod desktop_environment;
pub mod offset_time;
pub mod relative_time;
pub mod version;
pub mod numbers;

View file

@ -0,0 +1,54 @@
use derive_more::{
Add, AddAssign, Deref, Div, DivAssign, From, Mul, MulAssign, Sub, SubAssign,
};
use serde::{Serialize, Deserialize};
use std::fmt::{Debug, Display, Formatter, Result as FmtResult};
enum DisplayKind {
Plural(&'static str, &'static str),
HumanBytes,
}
macro_rules! define_fmt_wrapper {
($name:ident, $ty:ty, $display_kind:expr) => {
#[derive(
Copy, Clone, PartialEq, Eq, PartialOrd, Ord,
Add, Sub, Mul, Div,
AddAssign, SubAssign, MulAssign, DivAssign,
From, Deref, Serialize, Deserialize
)]
pub struct $name(pub $ty);
impl $name {
#[allow(dead_code)]
pub fn new(value: $ty) -> Self {
Self(value)
}
}
impl Display for $name {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
match $display_kind {
DisplayKind::Plural(singular, plural) => {
let label = if self.0 == 1 { singular } else { plural };
write!(f, "{} {}", self.0, label)
},
DisplayKind::HumanBytes => {
use humansize::{format_size, DECIMAL};
write!(f, "{}", format_size(self.0, DECIMAL))
},
}
}
}
impl Debug for $name {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self, f)
}
}
};
}
define_fmt_wrapper!(FmtThreads, u16, DisplayKind::Plural("Thread", "Threads"));
define_fmt_wrapper!(FmtCores, usize, DisplayKind::Plural("Core", "Cores"));
define_fmt_wrapper!(FmtBytes, u64, DisplayKind::HumanBytes);

View file

@ -0,0 +1,66 @@
use chrono::{DateTime, Local, Offset};
use derive_more::{Deref, Display, From};
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use std::{
cmp::Ordering,
fmt::{Debug, Display, Formatter, Result as FmtResult},
};
#[derive(Copy, Clone, From, Deref, Display)]
#[display("UTC {}", "_0.offset()")]
pub struct FmtOffsetTime(pub DateTime<Local>);
impl Debug for FmtOffsetTime {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self, f)
}
}
impl Serialize for FmtOffsetTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for FmtOffsetTime {
fn deserialize<D>(deserializer: D) -> Result<FmtOffsetTime, D::Error>
where
D: Deserializer<'de>,
{
// Deserialize the input as a string
let s = String::deserialize(deserializer)?;
// Attempt to parse the string into a DateTime<Local>
match DateTime::parse_from_rfc3339(&s) {
Ok(dt) => Ok(FmtOffsetTime(dt.with_timezone(&Local))),
Err(e) => Err(serde::de::Error::custom(format!(
"Failed to parse DateTime: {}",
e
))),
}
}
}
impl Eq for FmtOffsetTime {}
impl PartialEq for FmtOffsetTime {
fn eq(&self, other: &Self) -> bool {
self.0.offset().fix() == other.0.offset().fix()
}
}
impl PartialOrd for FmtOffsetTime {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(
self.0.offset().fix().local_minus_utc().cmp(&other.0.offset().fix().local_minus_utc()),
)
}
}
impl Ord for FmtOffsetTime {
fn cmp(&self, other: &Self) -> Ordering {
self.0.offset().fix().local_minus_utc().cmp(&other.0.offset().fix().local_minus_utc())
}
}

View file

@ -0,0 +1,50 @@
use derive_more::{From, Deref, with_trait::Display as Display};
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
time::Duration,
};
#[derive(
Clone,
PartialEq,
Eq,
PartialOrd,
Ord,
From,
Deref,
)]
pub struct FmtRelativeTime(pub Duration);
impl Display for FmtRelativeTime {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
let formatter = timeago::Formatter::new();
write!(f, "{}", formatter.convert(self.0))
}
}
impl Debug for FmtRelativeTime {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
Display::fmt(self, f)
}
}
impl Serialize for FmtRelativeTime {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_u64(self.0.as_secs())
}
}
impl<'de> Deserialize<'de> for FmtRelativeTime {
fn deserialize<D>(deserializer: D) -> Result<FmtRelativeTime, D::Error>
where
D: Deserializer<'de>,
{
let secs = u64::deserialize(deserializer)?;
Ok(FmtRelativeTime(Duration::from_secs(secs)))
}
}

View file

@ -0,0 +1,38 @@
use derive_more::{Display, From, Deref, with_trait::Display as TDisplay};
use serde::{Serialize, Serializer, Deserialize, Deserializer};
use std::{
fmt::{Debug, Formatter, Result as FmtResult},
str::FromStr,
};
use versions::Versioning;
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, From, Deref, Display)]
#[display("{_0}")]
pub struct FmtVersion(pub Versioning);
impl Debug for FmtVersion {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
TDisplay::fmt(self, f)
}
}
impl Serialize for FmtVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for FmtVersion {
fn deserialize<D>(deserializer: D) -> Result<FmtVersion, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Versioning::from_str(&s)
.map(FmtVersion)
.map_err(serde::de::Error::custom)
}
}

View file

@ -0,0 +1,131 @@
use super::UNKNOWN;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use wgpu;
use winit::{
application::ApplicationHandler,
event::WindowEvent,
event_loop::{ActiveEventLoop, EventLoop},
window::WindowId,
};
mod vram;
#[derive(Debug, Serialize, Deserialize)]
pub struct DriverInfo {
version: String,
name: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct AdapterInfo {
vendor: String,
model: String,
driver: DriverInfo,
vram: String,
display: DisplayInfo,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct GPUInfo {
supported_backends: Vec<String>,
gpus: Vec<AdapterInfo>,
}
#[derive(Debug, Serialize, Deserialize, Default)]
pub struct DisplayInfo {
resolution: String,
refresh_rate: String,
}
impl ApplicationHandler for DisplayInfo {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if let Some(monitor) = event_loop.primary_monitor() {
let size = monitor.size();
let refresh_rate = monitor.refresh_rate_millihertz();
self.resolution = format!("{}x{}", size.width, size.height);
self.refresh_rate = if let Some(refresh) = refresh_rate {
format!("{} hz", refresh / 1000)
} else {
UNKNOWN.to_string()
}
} else {
self.resolution = UNKNOWN.to_string();
self.refresh_rate = UNKNOWN.to_string();
}
event_loop.exit();
}
fn window_event(
&mut self,
_event_loop: &ActiveEventLoop,
_window_id: WindowId,
_event: WindowEvent,
) {
}
}
fn vendor_name(vendor: u32) -> &'static str {
match vendor {
0x10DE => "NVIDIA",
0x1002 => "AMD(Advanced Micro Devices), Inc.",
0x8086 => "Intel(integrated electronics)",
0x13B5 => "ARM(Advanced RISC Machines)",
0x5143 => "Qualcomm(Quality Communications)",
0x1010 => "Apple Inc.",
_ => UNKNOWN,
}
}
#[allow(dead_code)]
pub fn get_struct() -> GPUInfo {
let mut gpu_data: Vec<AdapterInfo> = Vec::new();
let mut backends: Vec<String> = Vec::new();
let instance_descriptor = wgpu::InstanceDescriptor {
backends: wgpu::Backends::all(),
flags: wgpu::InstanceFlags::empty(),
backend_options: Default::default(),
};
let instance = wgpu::Instance::new(&instance_descriptor);
let event_loop = EventLoop::new().unwrap();
let mut app = DisplayInfo::default();
event_loop.run_app(&mut app).unwrap();
for adapter in instance.enumerate_adapters(wgpu::Backends::all()) {
let info = adapter.get_info();
if !backends.contains(&info.backend.to_string()) {
backends.push(info.backend.to_string());
}
if info.driver.is_empty() || info.driver_info.is_empty() {
continue;
}
gpu_data.push(AdapterInfo {
vendor: vendor_name(info.vendor).to_string(),
model: info.name,
driver: DriverInfo {
version: info.driver_info,
name: info.driver,
},
vram: vram::get(info.vendor, info.device),
display: DisplayInfo {
resolution: app.resolution.to_string(),
refresh_rate: app.refresh_rate.to_string(),
},
});
}
GPUInfo {
supported_backends: backends,
gpus: gpu_data,
}
}
#[allow(dead_code)]
pub fn get_json() -> Value {
json!(get_struct())
}

View file

@ -0,0 +1,67 @@
use super::super::UNKNOWN;
use ash::vk::{API_VERSION_1_2, ApplicationInfo, InstanceCreateInfo, MemoryHeapFlags};
use humansize::{DECIMAL, make_format};
#[cfg(not(target_os = "macos"))]
pub fn get_metal() -> u64 {
0
}
#[cfg(target_os = "macos")]
pub fn get_metal() -> u64 {
use metal::Device as MetalDevice;
let device = MetalDevice::system_default().expect("No Metal-compatible GPU found");
device.recommended_max_working_set_size()
}
pub fn get_vulkan(device_id: u32) -> u64 {
let entry = unsafe { ash::Entry::load().unwrap() };
let app_info = ApplicationInfo {
p_application_name: std::ptr::null(),
application_version: 0,
p_engine_name: std::ptr::null(),
engine_version: 0,
api_version: API_VERSION_1_2,
..Default::default()
};
let create_info = InstanceCreateInfo {
p_application_info: &app_info,
..Default::default()
};
let instance = unsafe { entry.create_instance(&create_info, None).unwrap() };
let physical_devices = unsafe { instance.enumerate_physical_devices().unwrap() };
let mut total_vram = 0;
for device in physical_devices {
let memory_properties = unsafe { instance.get_physical_device_memory_properties(device) };
let device_properties = unsafe { instance.get_physical_device_properties(device) };
if device_id != device_properties.device_id {
continue;
}
for heap in memory_properties.memory_heaps {
if heap.flags.contains(MemoryHeapFlags::DEVICE_LOCAL) {
total_vram += heap.size;
}
}
break;
}
total_vram
}
pub fn get(vendor: u32, device_id: u32) -> String {
let formatter = make_format(DECIMAL);
match vendor {
0x10DE | 0x1002 | 0x8086 | 0x5143 => formatter(get_vulkan(device_id)),
0x1010 => formatter(get_metal()),
_ => UNKNOWN.to_string(),
}
}

View file

@ -0,0 +1,50 @@
use crate::modules::FmtBytes;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use sysinfo::System;
#[derive(Debug, Serialize, Deserialize)]
pub struct PhysicalInfo {
total: FmtBytes,
used: FmtBytes,
free: FmtBytes,
available: FmtBytes,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct VirtualInfo {
total: FmtBytes,
used: FmtBytes,
free: FmtBytes,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct MemoryInfo {
physical: PhysicalInfo,
virtual_swap: VirtualInfo,
}
#[allow(dead_code)]
pub fn get_struct() -> MemoryInfo {
let mut system = System::new_all();
system.refresh_memory();
MemoryInfo {
physical: PhysicalInfo {
total: FmtBytes(system.total_memory()),
used: FmtBytes(system.used_memory()),
free: FmtBytes(system.free_memory()),
available: FmtBytes(system.available_memory()),
},
virtual_swap: VirtualInfo {
total: FmtBytes(system.total_swap()),
used: FmtBytes(system.used_swap()),
free: FmtBytes(system.free_swap()),
},
}
}
#[allow(dead_code)]
pub fn get_json() -> Value {
json!(get_struct())
}

View file

@ -0,0 +1,40 @@
use crate::modules::FmtVersion;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use sys_locale::get_locale;
use thiserror::Error;
use versions::Versioning;
include!(concat!(env!("OUT_DIR"), "/built.rs"));
#[derive(Debug, Serialize, Deserialize)]
pub struct MetaInfo {
version: FmtVersion,
commit_hash: String,
locale: String,
}
#[derive(Debug, Error)]
pub enum MetaError {
#[error("Invalid Semver version")]
InvalidVersion,
#[error("Failed to build JSON for MetaInfo")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<MetaInfo, MetaError> {
let locale = get_locale().unwrap_or_else(|| String::from("en-US"));
let version = Versioning::new(env!("CARGO_PKG_VERSION")).ok_or(MetaError::InvalidVersion)?;
Ok(MetaInfo {
version: FmtVersion(version),
commit_hash: GIT_COMMIT_HASH.to_string(),
locale,
})
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, MetaError> {
Ok(json!(get_struct()?))
}

View file

@ -0,0 +1,26 @@
pub mod cpu;
pub mod devices;
pub mod external;
pub mod formatted;
pub mod gpu;
pub mod memory;
pub mod meta;
pub mod network;
pub mod os;
pub mod storage;
pub mod uptime;
#[allow(unused_imports)]
pub use formatted::{
architecture::FmtOSArchitecture,
numbers::FmtBytes,
date_time::{FmtDateTime, IntoDateTime},
desktop_environment::FmtDE,
offset_time::FmtOffsetTime,
relative_time::FmtRelativeTime,
version::FmtVersion,
numbers::FmtCores,
numbers::FmtThreads,
};
pub const UNKNOWN: &str = "Unknown";

View file

@ -0,0 +1,21 @@
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use sysinfo::Networks;
#[derive(Debug, Serialize, Deserialize)]
pub struct NetworkInfo {
adapters: Vec<String>,
}
#[allow(dead_code)]
pub fn get_struct() -> NetworkInfo {
let networks = Networks::new_with_refreshed_list();
let adapters = networks.iter().map(|(name, _)| name.to_string()).collect();
NetworkInfo { adapters }
}
#[allow(dead_code)]
pub fn get_json() -> Value {
json!(get_struct())
}

View file

@ -0,0 +1,59 @@
use super::UNKNOWN;
use crate::modules::{FmtDE, FmtOSArchitecture};
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
use sysinfo::System;
use thiserror::Error;
#[derive(Debug, Serialize, Deserialize)]
pub struct OSInfo {
name: String,
edition: String,
version: String,
architecture: FmtOSArchitecture,
kernel: Option<String>,
desktop_environment: Option<FmtDE>,
}
#[derive(Debug, Error)]
pub enum OSError {
#[error("Unsupported pointer width: 16 bit systems are not supported")]
UnsupportedPointerWidth,
#[error("Failed to build JSON for OSInfo")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<OSInfo, OSError> {
let architecture = match std::mem::size_of::<usize>() {
8 => Ok(FmtOSArchitecture::W64_BIT),
4 => Ok(FmtOSArchitecture::W32_BIT),
_ => Err(OSError::UnsupportedPointerWidth),
}?;
let kernel = if cfg!(target_os = "linux") || cfg!(target_os = "macos") {
Some(System::kernel_long_version())
} else {
None
};
let desktop_environment = if cfg!(target_os = "linux") {
FmtDE::detect()
} else {
None
};
Ok(OSInfo {
name: System::name().unwrap_or(UNKNOWN.to_string()),
edition: System::long_os_version().unwrap_or(UNKNOWN.to_string()),
version: System::os_version().unwrap_or(UNKNOWN.to_string()),
architecture,
kernel,
desktop_environment,
})
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, OSError> {
Ok(json!(get_struct()?))
}

View file

@ -0,0 +1,49 @@
use crate::modules::FmtBytes;
use serde::{Serialize, Deserialize};
use serde_json::{Value, json};
pub use sysinfo::DiskKind;
use sysinfo::Disks;
use thiserror::Error;
#[derive(Debug, Serialize, Deserialize)]
pub struct StorageInfo {
name: String,
mount_point: String,
disk_kind: DiskKind,
space_left: FmtBytes,
total: FmtBytes,
}
#[derive(Debug, Error)]
pub enum StorageError {
#[error("No storage drives found")]
NoDisksFound,
#[error("Failed to build JSON for Vec<StorageInfo>")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_list() -> Result<Vec<StorageInfo>, StorageError> {
let disks = Disks::new_with_refreshed_list();
let disk_list = disks.list();
if disk_list.is_empty() {
return Err(StorageError::NoDisksFound);
}
Ok(disk_list
.iter()
.map(|disk| StorageInfo {
name: disk.name().to_string_lossy().into_owned(),
mount_point: disk.mount_point().to_string_lossy().replace('\\', ""),
disk_kind: disk.kind(),
space_left: FmtBytes(disk.available_space()),
total: FmtBytes(disk.total_space()),
})
.collect())
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, StorageError> {
Ok(json!(get_list()?))
}

View file

@ -0,0 +1,71 @@
use crate::modules::{FmtDateTime, FmtOffsetTime, FmtRelativeTime};
use chrono::{DateTime, Local, Utc, TimeZone};
use serde::{Serialize, Serializer, Deserialize};
use serde_json::{Value, json};
use std::{
time::Duration as StdDuration,
fmt::Display,
};
use sysinfo::System;
use thiserror::Error;
fn serialize_datetime<S, Tz>(date: &DateTime<Tz>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
Tz::Offset: Display,
Tz: TimeZone,
{
let formatted = date.format("%+").to_string(); // Format as RFC 3339
serializer.serialize_str(&formatted)
}
#[derive(Debug, Serialize, Deserialize)]
struct DateInfo {
#[serde(serialize_with = "serialize_datetime")]
time_offset: FmtOffsetTime,
#[serde(serialize_with = "serialize_datetime")]
local_date_time: FmtDateTime<Local>,
#[serde(serialize_with = "serialize_datetime")]
utc_date_time: FmtDateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UptimeInfo {
boot: DateInfo,
now: DateInfo,
relative: FmtRelativeTime,
}
#[derive(Debug, Error)]
pub enum UptimeError {
#[error("Invalid or out of range timestamp: check seconds < 8_220_000_000_000")]
InvalidTimestamp,
#[error("Failed to build JSON for UptimeInfo")]
JsonError(#[from] serde_json::Error),
}
#[allow(dead_code)]
pub fn get_struct() -> Result<UptimeInfo, UptimeError> {
let boot_time_utc = DateTime::<Utc>::from_timestamp(System::boot_time() as i64, 0)
.ok_or(UptimeError::InvalidTimestamp)?;
let boot_time_local: DateTime<Local> = boot_time_utc.with_timezone(&Local);
let relative_time =
StdDuration::from_secs((Local::now() - boot_time_local).num_seconds() as u64);
let make_info = |utc_dt: DateTime<Utc>| DateInfo {
time_offset: FmtOffsetTime(Local::now()),
local_date_time: FmtDateTime(utc_dt.with_timezone(&Local)),
utc_date_time: FmtDateTime(utc_dt),
};
Ok(UptimeInfo {
boot: make_info(boot_time_utc),
now: make_info(Utc::now()),
relative: FmtRelativeTime(relative_time),
})
}
#[allow(dead_code)]
pub fn get_json() -> Result<Value, UptimeError> {
Ok(json!(get_struct()?))
}

View file

@ -74,7 +74,7 @@ impl LoggerConfig {
self.log_json_show_message = i;
self
}
pub fn log_json_show_additional_fields(mut self, i: bool) -> Self {
self.log_json_show_additional_fields = i;
self
@ -96,7 +96,7 @@ impl Default for LoggerConfig {
log_json_show_timestamp: false,
log_json_show_level: false,
log_json_show_message: false,
log_json_show_additional_fields: false
log_json_show_additional_fields: false,
}
}
}

View file

@ -113,7 +113,7 @@ where
level,
message,
#[cfg(feature = "json")]
additional_fields
additional_fields,
});
if let LogEvent::Log(ref entry) = log_entry {
@ -212,13 +212,7 @@ impl Logger {
for msg in rx {
match msg {
LogEvent::Log(mut entry) => {
println!(
"{}",
format_entry(
&mut entry,
&config_clone
)
);
println!("{}", format_entry(&mut entry, &config_clone));
}
LogEvent::Shutdown => break,
}
@ -298,12 +292,12 @@ fn format_entry(entry: &mut LogEntry, log_config: &LoggerConfig) -> String {
if log_config.log_use_json {
return format_entry_json(entry, log_config);
}
if log_config.log_to_stdout || log_config.log_to_file {
return format_entry_string(entry, log_config);
} else {
return String::new();
}
}
}
fn format_entry_string(entry: &LogEntry, log_config: &LoggerConfig) -> String {
@ -327,7 +321,10 @@ fn format_entry_json(entry: &mut LogEntry, log_config: &LoggerConfig) -> String
let mut json_object = serde_json::Map::new();
if log_config.log_json_show_timestamp {
json_object.insert("timestamp".to_string(), Value::String(DateTime::<Utc>::from(entry.timestamp).to_rfc3339()));
json_object.insert(
"timestamp".to_string(),
Value::String(DateTime::<Utc>::from(entry.timestamp).to_rfc3339()),
);
}
if log_config.log_json_show_level {
@ -335,7 +332,10 @@ fn format_entry_json(entry: &mut LogEntry, log_config: &LoggerConfig) -> String
}
if log_config.log_json_show_message {
json_object.insert("message".to_string(), Value::String(entry.message.to_string()));
json_object.insert(
"message".to_string(),
Value::String(entry.message.to_string()),
);
}
if log_config.log_json_show_additional_fields {
@ -343,4 +343,4 @@ fn format_entry_json(entry: &mut LogEntry, log_config: &LoggerConfig) -> String
}
serde_json::to_string(&json_object).unwrap()
}
}

View file

@ -1,7 +1,7 @@
use pretty_assertions::assert_eq;
use tracing::Level;
use serde::{Deserialize, Serialize};
use serde_json::Map;
use serde::{Serialize, Deserialize};
use tracing::Level;
use super::*;
@ -126,9 +126,15 @@ fn test_logger_sequential_consistency_json() {
for log in logger.get_logs(LogQuery::All) {
let mut json_object = serde_json::Map::new();
json_object.insert("timestamp".to_string(), Value::String(DateTime::<Utc>::from(log.timestamp).to_rfc3339()));
json_object.insert(
"timestamp".to_string(),
Value::String(DateTime::<Utc>::from(log.timestamp).to_rfc3339()),
);
json_object.insert("level".to_string(), Value::String(log.level.to_string()));
json_object.insert("message".to_string(), Value::String(log.message.to_string()));
json_object.insert(
"message".to_string(),
Value::String(log.message.to_string()),
);
log_json.push(json_object);
}
@ -136,4 +142,4 @@ fn test_logger_sequential_consistency_json() {
for log in log_json {
serde_json::to_string(&log).unwrap();
}
}
}