blob: 206e59f35d2e09810db5d2caeead99e92c306aa6 [file]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//! Process- and host-level probe behind the `GetStats` reply: `sysinfo`
//! sampling of this process, the cached host identity, and disk usage of the
//! volume holding the data directory.
use std::cell::RefCell;
use std::path::PathBuf;
use std::sync::OnceLock;
use sysinfo::System as SysinfoSystem;
use system_stats::SystemProbe;
/// Process- and host-level portion of the stats reply, probed via `sysinfo`.
/// These describe the whole process, not shard or metadata state, so any one
/// shard can serve them without aggregation. The CPU fields are deltas over the
/// serving thread's own [`SYSINFO`] refresh history, so they vary by serving
/// shard (a shard's first probe reports zero CPU).
pub struct SystemStats {
pub process_id: u32,
pub cpu_usage: f32,
pub total_cpu_usage: f32,
pub memory_usage: u64,
pub total_memory: u64,
pub available_memory: u64,
pub run_time: u64,
pub start_time: u64,
pub read_bytes: u64,
pub written_bytes: u64,
pub threads_count: u32,
pub hostname: String,
pub os_name: String,
pub os_version: String,
pub kernel_version: String,
}
thread_local! {
// `cpu_usage` is a delta since the previous refresh, so the sampled
// `System` is kept alive across `GetStats` calls (a freshly created one
// reports zero CPU). Mirrors the legacy shard-0 stats path.
static SYSINFO: RefCell<Option<SysinfoSystem>> = const { RefCell::new(None) };
}
/// Host / OS identity is process-static (unlike the per-call CPU and memory
/// samples), so probe it once and clone from the cache on each `GetStats`
/// rather than re-querying sysinfo every call. Process-global, so a `OnceLock`
/// fits better than the per-thread [`SYSINFO`] cell.
struct HostIdentity {
hostname: String,
os_name: String,
os_version: String,
kernel_version: String,
}
impl HostIdentity {
fn probe() -> Self {
Self {
hostname: SysinfoSystem::host_name().unwrap_or_else(|| "unknown_hostname".to_owned()),
os_name: SysinfoSystem::name().unwrap_or_else(|| "unknown_os_name".to_owned()),
os_version: SysinfoSystem::long_os_version()
.unwrap_or_else(|| "unknown_os_version".to_owned()),
kernel_version: SysinfoSystem::kernel_version()
.unwrap_or_else(|| "unknown_kernel_version".to_owned()),
}
}
}
static HOST_IDENTITY: OnceLock<HostIdentity> = OnceLock::new();
/// Configured data directory, captured once at bootstrap so the sync stats
/// read path can report disk usage of the volume that holds iggy data rather
/// than an unrelated mount. Process-global because the shard does not carry
/// server config on the read path. Unset (disk stats fall back to 0) until
/// bootstrap.
static STATS_DATA_PATH: OnceLock<PathBuf> = OnceLock::new();
/// Capture the configured data directory for `GetStats` disk reporting.
/// Idempotent: only the first call (process bootstrap) takes effect.
pub fn init_stats_data_path(path: PathBuf) {
let _ = STATS_DATA_PATH.set(path);
}
/// Free and total bytes of the volume holding the configured data directory,
/// `(0, 0)` before bootstrap or on a probe error.
pub fn stats_disk_space() -> (u64, u64) {
STATS_DATA_PATH.get().map_or((0, 0), |path| {
(
fs2::available_space(path).unwrap_or(0),
fs2::total_space(path).unwrap_or(0),
)
})
}
pub fn probe_system_stats() -> SystemStats {
let host = HOST_IDENTITY.get_or_init(HostIdentity::probe);
let probe = SYSINFO.with_borrow_mut(|slot| {
let sys = slot.get_or_insert_with(SysinfoSystem::new);
SystemProbe::capture(sys)
});
SystemStats {
process_id: probe.process_id,
cpu_usage: probe.cpu_usage,
total_cpu_usage: probe.total_cpu_usage,
memory_usage: probe.memory_usage,
total_memory: probe.total_memory,
available_memory: probe.available_memory,
// sysinfo reports whole seconds; the wire fields are micros (the
// SDK decodes them via `IggyDuration` / `IggyTimestamp::from`, both
// micro-based).
run_time: probe.run_time_secs.saturating_mul(1_000_000),
start_time: probe.start_time_secs.saturating_mul(1_000_000),
read_bytes: probe.read_bytes,
written_bytes: probe.written_bytes,
threads_count: probe.threads_count,
hostname: host.hostname.clone(),
os_name: host.os_name.clone(),
os_version: host.os_version.clone(),
kernel_version: host.kernel_version.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn probe_system_stats_reports_this_process_and_host_memory() {
let stats = probe_system_stats();
// Straight from `sysinfo`, independent of shard state: the pid is our
// own and any host the test runs on has nonzero total memory. A zero
// here means the probe wired nothing (the pre-fix stubbed literal).
assert_eq!(stats.process_id, std::process::id());
assert!(stats.total_memory > 0);
assert!(!stats.hostname.is_empty());
}
}