feat: record-daemon logs to a file

This commit is contained in:
2026-05-30 10:41:40 +02:00
parent 7f54e959b8
commit 598c5a2391
5 changed files with 108 additions and 7 deletions
+21
View File
@@ -2418,6 +2418,7 @@ dependencies = [
"tokio-util", "tokio-util",
"toml", "toml",
"tracing", "tracing",
"tracing-appender",
"tracing-subscriber", "tracing-subscriber",
"uuid", "uuid",
"winapi 0.3.9", "winapi 0.3.9",
@@ -3074,6 +3075,12 @@ version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "symlink"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.117" version = "2.0.117"
@@ -3255,6 +3262,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
dependencies = [ dependencies = [
"deranged", "deranged",
"itoa",
"num-conv", "num-conv",
"powerfmt", "powerfmt",
"serde_core", "serde_core",
@@ -3520,6 +3528,19 @@ dependencies = [
"tracing-core", "tracing-core",
] ]
[[package]]
name = "tracing-appender"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
dependencies = [
"crossbeam-channel",
"symlink",
"thiserror 2.0.18",
"time",
"tracing-subscriber",
]
[[package]] [[package]]
name = "tracing-attributes" name = "tracing-attributes"
version = "0.1.31" version = "0.1.31"
+1
View File
@@ -34,6 +34,7 @@ anyhow = "1"
# Logging # Logging
tracing = "0.1" tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
tracing-appender = "0.2"
# UUID for recording IDs # UUID for recording IDs
uuid = { version = "1", features = ["v4", "serde"] } uuid = { version = "1", features = ["v4", "serde"] }
+45 -7
View File
@@ -42,6 +42,10 @@ struct Args {
/// Socket path for IPC. /// Socket path for IPC.
#[arg(short, long)] #[arg(short, long)]
socket: Option<std::path::PathBuf>, socket: Option<std::path::PathBuf>,
/// Path to log file. If not specified, logs to stdout/stderr.
#[arg(short, long, value_name = "PATH")]
log_file: Option<std::path::PathBuf>,
} }
/// Main daemon structure. /// Main daemon structure.
@@ -579,15 +583,49 @@ impl Daemon {
} }
} }
/// Initialize logging. /// Initialize logging with optional file output.
fn init_logging(level: &str) { fn init_logging(level: &str, log_file: Option<&std::path::Path>) {
let filter = tracing_subscriber::EnvFilter::try_from_default_env() let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level)); .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new(level));
tracing_subscriber::registry() if let Some(log_path) = log_file {
.with(filter) // Ensure parent directory exists
.with(tracing_subscriber::fmt::layer()) if let Some(parent) = log_path.parent() {
.init(); let _ = std::fs::create_dir_all(parent);
}
// Use file appender for logging
let prefix = log_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("daemon");
let file_appender = tracing_appender::rolling::RollingFileAppender::builder()
.rotation(tracing_appender::rolling::Rotation::DAILY)
.filename_prefix(prefix)
.filename_suffix("log")
.build(log_path.parent().unwrap_or(std::path::Path::new(".")))
.expect("Failed to create log file appender");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
// Store the guard to prevent it from being dropped
// We leak it intentionally to keep logging working for the daemon's lifetime
std::mem::forget(_guard);
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer().with_writer(non_blocking))
.init();
eprintln!("Logging to file: {}", log_path.display());
} else {
// Log to stdout/stderr
tracing_subscriber::registry()
.with(filter)
.with(tracing_subscriber::fmt::layer())
.init();
}
// Set up panic hook to log panics // Set up panic hook to log panics
std::panic::set_hook(Box::new(|panic_info| { std::panic::set_hook(Box::new(|panic_info| {
@@ -614,7 +652,7 @@ async fn main() -> Result<()> {
let args = Args::parse(); let args = Args::parse();
// Initialize logging // Initialize logging
init_logging(&args.log_level); init_logging(&args.log_level, args.log_file.as_deref());
info!("Record Daemon v{} starting", record_daemon::VERSION); info!("Record Daemon v{} starting", record_daemon::VERSION);
+39
View File
@@ -314,6 +314,20 @@ fn find_daemon_binary() -> Option<PathBuf> {
None None
} }
/// Get the log file path for the daemon.
///
/// Returns a path in the app's data directory.
fn get_log_file_path() -> Result<PathBuf, String> {
let data_dir = directories::ProjectDirs::from("com", "leaguerecorder", "record-daemon")
.ok_or_else(|| "Failed to get app data directory".to_string())?;
let log_dir = data_dir.data_dir().join("logs");
std::fs::create_dir_all(&log_dir)
.map_err(|e| format!("Failed to create log directory: {}", e))?;
Ok(log_dir.join("daemon.log"))
}
/// Spawn the record-daemon as a detached background process. /// Spawn the record-daemon as a detached background process.
/// ///
/// Returns `Ok(())` if the process was successfully spawned. /// Returns `Ok(())` if the process was successfully spawned.
@@ -321,10 +335,13 @@ fn spawn_daemon() -> Result<(), String> {
let binary = find_daemon_binary() let binary = find_daemon_binary()
.ok_or_else(|| "record-daemon binary not found".to_string())?; .ok_or_else(|| "record-daemon binary not found".to_string())?;
let log_file = get_log_file_path()?;
eprintln!("[daemon] Spawning record-daemon: {}", binary.display()); eprintln!("[daemon] Spawning record-daemon: {}", binary.display());
eprintln!("[daemon] Log file: {}", log_file.display());
let mut cmd = std::process::Command::new(&binary); let mut cmd = std::process::Command::new(&binary);
cmd.arg("--foreground"); cmd.arg("--foreground");
cmd.arg("--log-file").arg(&log_file);
// Set the working directory to the daemon binary's directory so it can // Set the working directory to the daemon binary's directory so it can
// find its bundled resources (OBS plugins, etc.) relative to itself. // find its bundled resources (OBS plugins, etc.) relative to itself.
@@ -536,3 +553,25 @@ pub async fn daemon_shutdown() -> Result<serde_json::Value, String> {
Err(response.error.unwrap_or_else(|| "Unknown error".to_string())) Err(response.error.unwrap_or_else(|| "Unknown error".to_string()))
} }
} }
/// Get the daemon log file path.
#[tauri::command]
pub fn daemon_get_log_path() -> Result<String, String> {
get_log_file_path()
.map(|p| p.to_string_lossy().to_string())
}
/// Read the daemon log file.
///
/// Returns the last `lines` lines of the log file (default 100, max 1000).
#[tauri::command]
pub fn daemon_read_logs(lines: Option<usize>) -> Result<String, String> {
let log_path = get_log_file_path()?;
let max_lines = lines.unwrap_or(100).min(1000);
let content = std::fs::read_to_string(&log_path)
.map_err(|e| format!("Failed to read log file: {}", e))?;
let lines: Vec<&str> = content.lines().rev().take(max_lines).collect();
Ok(lines.into_iter().rev().collect::<Vec<_>>().join("\n"))
}
+2
View File
@@ -439,6 +439,8 @@ pub fn run() {
daemon_ipc::daemon_start_recording, daemon_ipc::daemon_start_recording,
daemon_ipc::daemon_stop_recording, daemon_ipc::daemon_stop_recording,
daemon_ipc::daemon_shutdown, daemon_ipc::daemon_shutdown,
daemon_ipc::daemon_get_log_path,
daemon_ipc::daemon_read_logs,
]) ])
.run(tauri::generate_context!()) .run(tauri::generate_context!())
.expect("error while running tauri application"); .expect("error while running tauri application");