use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use chrono::{FixedOffset, Utc};
use tracing_subscriber::fmt::time::FormatTime;
use tracing_subscriber::{fmt, EnvFilter};
const JST_OFFSET_SECS: i32 = 9 * 3600;
fn jst() -> FixedOffset {
FixedOffset::east_opt(JST_OFFSET_SECS).expect("invalid JST offset")
}
struct JstTimer {
session_key: String,
}
impl FormatTime for JstTimer {
fn format_time(&self, w: &mut tracing_subscriber::fmt::format::Writer<'_>) -> std::fmt::Result {
let now = Utc::now().with_timezone(&jst());
write!(
w,
"{} [{}]",
now.format("%Y-%m-%dT%H:%M:%S%.3f+09:00"),
self.session_key
)
}
}
struct JstRollingAppender {
dir: PathBuf,
prefix: String,
current_date: chrono::NaiveDate,
file: File,
}
impl JstRollingAppender {
fn new(dir: PathBuf, prefix: &str) -> std::io::Result<Self> {
let today = Utc::now().with_timezone(&jst()).date_naive();
let file = Self::open_log_file(&dir, prefix, today)?;
Ok(Self {
dir,
prefix: prefix.to_string(),
current_date: today,
file,
})
}
fn open_log_file(dir: &Path, prefix: &str, date: chrono::NaiveDate) -> std::io::Result<File> {
let filename = format!("{}_{}.log", prefix, date.format("%Y-%m-%d"));
OpenOptions::new()
.create(true)
.append(true)
.open(dir.join(filename))
}
}
impl Write for JstRollingAppender {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
let today = Utc::now().with_timezone(&jst()).date_naive();
if today != self.current_date {
self.file = Self::open_log_file(&self.dir, &self.prefix, today)?;
self.current_date = today;
}
self.file.write(buf)
}
fn flush(&mut self) -> std::io::Result<()> {
self.file.flush()
}
}
fn log_dir() -> PathBuf {
crate::storage::BlackBox::data_dir().join("logs")
}
pub fn init_logging(
log_dir_override: Option<PathBuf>,
session_key: &str,
) -> (tracing_appender::non_blocking::WorkerGuard, bool) {
let log_dir = log_dir_override.unwrap_or_else(log_dir);
if let Err(e) = std::fs::create_dir_all(&log_dir) {
eprintln!(
"jarvish: warning: failed to create log directory {}: {e}",
log_dir.display()
);
}
let (writer, operational): (Box<dyn Write + Send>, bool) =
match JstRollingAppender::new(log_dir.clone(), "jarvish") {
Ok(appender) => (Box::new(appender), true),
Err(e) => {
eprintln!(
"jarvish: warning: failed to create log file in {}: {e}",
log_dir.display()
);
(Box::new(std::io::sink()), false)
}
};
let (non_blocking, guard) = tracing_appender::non_blocking(writer);
let env_filter =
EnvFilter::try_from_env("JARVISH_LOG").unwrap_or_else(|_| EnvFilter::new("debug"));
fmt()
.with_env_filter(env_filter)
.with_writer(non_blocking)
.with_timer(JstTimer {
session_key: session_key.to_string(),
})
.with_ansi(false) .with_target(true) .with_thread_ids(false)
.with_line_number(true) .with_file(true) .init();
(guard, operational)
}
pub fn start_cpu_monitor() {
use std::time::Duration;
use sysinfo::{ProcessRefreshKind, ProcessesToUpdate};
const INTERVAL: Duration = Duration::from_secs(2);
const CPU_THRESHOLD: f32 = 10.0;
std::thread::spawn(move || {
let pid = match sysinfo::get_current_pid() {
Ok(pid) => pid,
Err(e) => {
tracing::warn!("[CPU Monitor] Failed to get current PID: {e}");
return;
}
};
let refresh_kind = ProcessRefreshKind::nothing().with_cpu();
let pids = [pid];
let target = ProcessesToUpdate::Some(&pids);
let mut sys = sysinfo::System::new();
sys.refresh_processes_specifics(target, false, refresh_kind);
std::thread::sleep(INTERVAL);
loop {
sys.refresh_processes_specifics(target, false, refresh_kind);
if let Some(process) = sys.process(pid) {
let usage = process.cpu_usage();
if usage >= CPU_THRESHOLD {
tracing::warn!("[CPU Monitor] High CPU usage detected: {usage:.1}%");
} else {
tracing::debug!("[CPU Monitor] CPU usage: {usage:.1}%");
}
}
std::thread::sleep(INTERVAL);
}
});
}