diff --git a/Cargo.lock b/Cargo.lock index 3d08bd2..2a2cc87 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_term" version = "0.11.0" @@ -40,6 +64,12 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "bytes" version = "1.1.0" @@ -48,9 +78,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "cc" -version = "1.0.70" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d26a6ce4b6a484fa3edb70f7efa6fc430fd2b87285fe8b84304fd0936faa0dc0" +checksum = "e9e8aabfac534be767c909e0690571677d49f41bd8465ae876fe043d52ba5292" [[package]] name = "cfg-if" @@ -58,6 +88,20 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-targets", +] + [[package]] name = "clap" version = "2.33.3" @@ -83,6 +127,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "dtoa" version = "0.4.8" @@ -129,7 +179,7 @@ dependencies = [ "proc-macro-hack", "proc-macro2", "quote", - "syn", + "syn 1.0.76", ] [[package]] @@ -147,6 +197,29 @@ dependencies = [ "libc", ] +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -172,6 +245,15 @@ version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +[[package]] +name = "js-sys" +version = "0.3.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "libc" version = "0.2.139" @@ -204,9 +286,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "memoffset" @@ -261,6 +343,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.13.0" @@ -273,9 +364,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" [[package]] name = "parking_lot" @@ -316,18 +407,18 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.29" +version = "1.0.88" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" +checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "quote" -version = "1.0.9" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] @@ -341,6 +432,35 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "ryu" version = "1.0.5" @@ -370,7 +490,7 @@ checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.76", ] [[package]] @@ -434,6 +554,17 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "syn" +version = "2.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89132cd0bf050864e1d38dc3bbc07a0eb8e7530af26344d3d2bbbef83499f590" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -460,7 +591,7 @@ checksum = "bad553cc2c78e8de258400763a647e80e6d1b31ee237275d756f6836d204494c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.76", ] [[package]] @@ -491,7 +622,7 @@ checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.76", ] [[package]] @@ -520,6 +651,12 @@ dependencies = [ "tokio", ] +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + [[package]] name = "unicode-width" version = "0.1.9" @@ -538,6 +675,61 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +[[package]] +name = "wasm-bindgen" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.79", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d" + [[package]] name = "winapi" version = "0.3.9" @@ -560,6 +752,79 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "yaml-rust" version = "0.4.5" @@ -574,12 +839,14 @@ name = "zinit" version = "0.2.0" dependencies = [ "anyhow", + "chrono", "clap", "command-group", "fern", "git-version", "log", "nix", + "regex", "serde", "serde_json", "serde_yaml", diff --git a/Cargo.toml b/Cargo.toml index 822ddb5..74a31c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,3 +20,5 @@ thiserror = "1.0" clap = "2.33" git-version = "0.3.5" command-group = "1.0.8" +chrono = "0.4.38" +regex = "1.11.0" diff --git a/src/app/api.rs b/src/app/api.rs index 38ec5c6..3fb3a7e 100644 --- a/src/app/api.rs +++ b/src/app/api.rs @@ -156,11 +156,18 @@ impl Api { Ok(encoder::to_value(map)?) } - async fn monitor>(name: S, zinit: ZInit) -> Result { - let (name, service) = config::load(format!("{}.yaml", name.as_ref())) - .context("failed to load service config")?; - zinit.monitor(name, service).await?; - Ok(Value::Null) + async fn monitor>(service_name: S, zinit: ZInit) -> Result { + match config::load(format!("{}.yaml", service_name.as_ref())) { + Ok((name, service)) => { + zinit.monitor(name, service).await?; + Ok(Value::Null) + } + Err(e) => bail!( + "Failed to load service config for '{}': {}", + service_name.as_ref(), + e + ), + } } async fn forget>(name: S, zinit: ZInit) -> Result { diff --git a/src/manager/filelogger.rs b/src/manager/filelogger.rs new file mode 100644 index 0000000..f7afc39 --- /dev/null +++ b/src/manager/filelogger.rs @@ -0,0 +1,183 @@ +use std::path::PathBuf; + +use anyhow::{Context, Result}; +use chrono::Local; +use regex::Regex; +use tokio::fs::{File, OpenOptions}; +use tokio::io::AsyncWriteExt; + +const MAX_LOG_FILES: usize = 5; +const DEFAULT_LOG_DIR: &str = "/var/log/zinit"; + +pub struct RotatingFileLogger { + log_file_path: PathBuf, + size_threshold: u64, + current_size: u64, + file: File, + rotated_file_pattern: Regex, + max_rotated_files: usize, +} + +impl RotatingFileLogger { + pub async fn new

(service_name: &str, log_file_name: P, size_threshold: u64) -> Result + where + P: Into, + { + let file_path: PathBuf = log_file_name.into(); + + // Check if correct .txt extension + if let Some(ext) = file_path.extension() { + if ext != "txt" { + bail!("Log file must have .txt extension"); + } + } + + // Log file will be saved under `/var/log/zinit//` + let mut log_file_path = PathBuf::new(); + log_file_path.push(format!("{}/{}", DEFAULT_LOG_DIR, service_name)); + log_file_path.push(file_path.clone()); + debug!( + "Creating log file for {} at {}", + service_name, + log_file_path.to_string_lossy() + ); + + // Create the directory if it doesn't exist + if let Some(parent) = log_file_path.parent() { + tokio::fs::create_dir_all(parent).await.with_context(|| { + format!("Failed to create directories for log file: {:?}", parent) + })?; + } + + // Bail if there is already a log file with the same name + if tokio::fs::metadata(&log_file_path).await.is_ok() { + bail!("Another log file with the same name already exists; change the log file name"); + } + + // Open or create the log file + let file = OpenOptions::new() + .create(true) + .append(true) + .open(&log_file_path) + .await + .with_context(|| format!("Failed to open log file: {:?}", log_file_path))?; + + // Get current file size + let metadata = file.metadata().await?; + let current_size = metadata.len(); + + // Compile regex to match rotated log files + // Example: log_20230826_123456.txt + let rotated_file_pattern = if let Some(base_name) = log_file_path.file_name() { + let pattern = format!( + r"^{}_\d{{8}}_\d{{6}}\.txt$", + regex::escape(base_name.to_str().unwrap().trim_end_matches(".txt")) + ); + Regex::new(&pattern)? + } else { + return Err(anyhow!("Failed to compile regex pattern")); + }; + + Ok(RotatingFileLogger { + log_file_path, + size_threshold, + current_size, + file, + rotated_file_pattern, + max_rotated_files: MAX_LOG_FILES, + }) + } + + pub async fn push(&mut self, log_line: &str) -> Result<()> { + let log_line_size = log_line.len() as u64 + 1; // +1 for newline + if log_line_size + self.current_size > self.size_threshold { + self.rotate().await?; + } + + self.file.write_all(log_line.as_bytes()).await?; + self.file.write_all(b"\n").await?; + self.file.flush().await?; + + self.current_size += log_line_size; + Ok(()) + } + + async fn rotate(&mut self) -> Result<()> { + // Close current file + self.file.sync_all().await?; + + // Generate new filename with date and time + let now = Local::now(); + let datetime_str = now.format("%Y%m%d_%H%M%S").to_string(); + let base_stem = self + .log_file_path + .file_stem() + .and_then(|s| s.to_str()) + .unwrap_or(""); + let rotated_file_name = format!("{}_{}.txt", base_stem, datetime_str); + let rotated_file_path = self.log_file_path.with_file_name(rotated_file_name); + + // Rename the current file to teh rotated filename + tokio::fs::rename(&self.log_file_path, &rotated_file_path) + .await + .with_context(|| format!("Failed to rename log file to {:?}", rotated_file_path))?; + + // Open a new file with the original name + self.file = OpenOptions::new() + .create(true) + .append(true) + .open(&self.log_file_path) + .await + .with_context(|| format!("Failed to open new log file: {:?}", self.log_file_path))?; + + // After rotating, reset current size 0 + self.current_size = 0; + + // Enforce the maximum number of rotated files + self.enforce_max_rotated_files().await?; + + Ok(()) + } + + async fn enforce_max_rotated_files(&self) -> Result<()> { + let dir = self + .log_file_path + .parent() + .ok_or_else(|| anyhow::anyhow!("Failed to get log file directory"))?; + + let mut entries = tokio::fs::read_dir(dir).await?; + let mut rotated_files = Vec::new(); + + // Find the rotated files based on the log file name + while let Some(entry) = entries.next_entry().await? { + let path = entry.path(); + if path.is_file() { + if let Some(filename) = path.file_name().and_then(|s| s.to_str()) { + if self.rotated_file_pattern.is_match(filename) { + // retrieve modification time + if let Ok(metadata) = tokio::fs::metadata(&path).await { + if let Ok(modified) = metadata.modified() { + rotated_files.push((path, modified)); + } + } + } + } + } + } + + // Sort by creation time + rotated_files.sort_by_key(|(_, modtime)| *modtime); + + // If there are more than max_rotated_files, delete the oldest ones + if rotated_files.len() > self.max_rotated_files { + let files_to_delete = &rotated_files[..rotated_files.len() - self.max_rotated_files]; + for (file, _) in files_to_delete { + tokio::fs::remove_file(file).await.with_context(|| { + format!("Failed to delete old rotated log file: {:?}", file) + })?; + } + } + + Ok(()) + } +} diff --git a/src/manager/mod.rs b/src/manager/mod.rs index b670e20..6fa87e3 100644 --- a/src/manager/mod.rs +++ b/src/manager/mod.rs @@ -2,12 +2,14 @@ use std::collections::HashMap; use anyhow::{Context, Result}; use command_group::CommandGroup; +use filelogger::RotatingFileLogger; use nix::sys::signal; use nix::sys::wait::{self, WaitStatus}; use nix::unistd::Pid; use std::fs::File as StdFile; use std::os::unix::io::FromRawFd; use std::os::unix::io::IntoRawFd; +use std::path::PathBuf; use std::process::Command; use std::process::Stdio; use std::sync::Arc; @@ -19,8 +21,11 @@ use tokio::sync::oneshot; use tokio::sync::Mutex; mod buffer; +mod filelogger; pub use buffer::Logs; +const MAX_LOG_FILE_SIZE: u64 = 1024 * 1024; // 1 MiB + pub struct Process { cmd: String, env: HashMap, @@ -65,6 +70,7 @@ pub enum Log { None, Stdout, Ring(String), + File(PathBuf), } #[derive(Clone)] @@ -72,6 +78,7 @@ pub struct ProcessManager { table: Arc>>, ring: buffer::Ring, env: Environ, + loggers: Arc>>>>, } impl ProcessManager { @@ -80,6 +87,7 @@ impl ProcessManager { table: Arc::new(Mutex::new(HashMap::new())), ring: buffer::Ring::new(cap), env: Environ::new(), + loggers: Arc::new(Mutex::new(HashMap::new())), } } @@ -126,17 +134,27 @@ impl ProcessManager { }); } - fn sink(&self, file: File, prefix: String) { + fn sink(&self, file: File, logger: Option>>, prefix: String) { let ring = self.ring.clone(); + let prefix_clone = prefix.clone(); let reader = BufReader::new(file); tokio::spawn(async move { let mut lines = reader.lines(); while let Ok(line) = lines.next_line().await { - let _ = match line { - Some(line) => ring.push(format!("{}: {}", prefix, line)).await, + match line { + Some(line) => { + let log_line = format!("{}: {}", prefix_clone, line); + // write to kernel buffer + let _ = ring.push(log_line.clone()).await; + if let Some(logger) = &logger { + let mut logger = logger.lock().await; + // write to log file + let _ = logger.push(&log_line).await; + } + } None => break, - }; + } } }); } @@ -149,7 +167,7 @@ impl ProcessManager { Ok(signal::killpg(pid, sig)?) } - pub async fn run(&self, cmd: Process, log: Log) -> Result { + pub async fn run(&self, cmd: Process, log: Log, service_name: &str) -> Result { let args = shlex::split(&cmd.cmd).context("failed to parse command")?; if args.is_empty() { bail!("invalid command"); @@ -167,7 +185,7 @@ impl ProcessManager { let child = match log { Log::None => child.stdout(Stdio::null()).stderr(Stdio::null()), - Log::Ring(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()), + Log::Ring(_) | Log::File(_) => child.stdout(Stdio::piped()).stderr(Stdio::piped()), _ => child, // default to inherit }; @@ -178,21 +196,39 @@ impl ProcessManager { .context("failed to spawn command")? .into_inner(); - if let Log::Ring(prefix) = log { - let _ = self - .ring - .push(format!("[-] {}: ------------ [start] ------------", prefix)) - .await; - - if let Some(out) = child.stdout.take() { - let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); - self.sink(out, format!("[+] {}", prefix)) + match log { + Log::Ring(prefix) => { + self.log_service_output(&prefix, None, &mut child).await; } + Log::File(ref log_file_path) => { + let mut loggers = self.loggers.lock().await; + + // Check if logger exists already or create a new one + let logger = if let Some(existing_logger) = loggers.get(log_file_path) { + Arc::clone(existing_logger) + } else { + let new_logger = Arc::new(Mutex::new( + RotatingFileLogger::new( + service_name, + log_file_path.clone(), + MAX_LOG_FILE_SIZE, + ) + .await?, + )); + + loggers.insert(log_file_path.clone(), Arc::clone(&new_logger)); + + new_logger + }; - if let Some(out) = child.stderr.take() { - let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); - self.sink(out, format!("[-] {}", prefix)) + drop(loggers); + + let prefix_str = log_file_path.to_string_lossy().to_string(); + + self.log_service_output(&prefix_str, Some(Arc::clone(&logger)), &mut child) + .await; } + _ => {} } let (tx, rx) = oneshot::channel(); @@ -204,6 +240,28 @@ impl ProcessManager { Ok(Child::new(pid, rx)) } + + async fn log_service_output( + &self, + prefix: &str, + logger: Option>>, + child: &mut std::process::Child, + ) { + let _ = self + .ring + .push(format!("[-] {}: ------------ [start] ------------", prefix)) + .await; + + if let Some(out) = child.stdout.take() { + let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); + self.sink(out, logger.clone(), format!("[+] {}", prefix)); + } + + if let Some(out) = child.stderr.take() { + let out = File::from_std(unsafe { StdFile::from_raw_fd(out.into_raw_fd()) }); + self.sink(out, logger.clone(), format!("[-] {}", prefix)); + } + } } #[derive(Clone)] diff --git a/src/zinit/config.rs b/src/zinit/config.rs index f706c47..1d400df 100644 --- a/src/zinit/config.rs +++ b/src/zinit/config.rs @@ -30,6 +30,7 @@ pub enum Log { #[default] Ring, Stdout, + File, } fn default_shutdown_timeout_fn() -> u64 { @@ -50,6 +51,7 @@ pub struct Service { pub after: Vec, pub signal: Signal, pub log: Log, + pub log_file: Option, pub env: HashMap, pub dir: String, } @@ -62,6 +64,16 @@ impl Service { bail!("missing exec directive"); } + // Validate `log_file` when `log` is `File` + if let Log::File = self.log { + if self.log_file.is_none() { + bail!("log_file must be specified when log is set to 'file'"); + } + if !(self.log_file.clone().unwrap().ends_with(".txt")) { + bail!("log_file must have .txt extension"); + } + } + Signal::from_str(&self.signal.stop.to_uppercase())?; Ok(()) diff --git a/src/zinit/mod.rs b/src/zinit/mod.rs index 421525a..a8ff0f9 100644 --- a/src/zinit/mod.rs +++ b/src/zinit/mod.rs @@ -506,6 +506,7 @@ impl ZInit { config::Log::None => Log::None, config::Log::Stdout => Log::Stdout, config::Log::Ring => Log::Ring(format!("{}/test", name.as_ref())), + config::Log::File => Log::File(format!("{}_test", name.as_ref()).into()), }; let test = self @@ -513,6 +514,7 @@ impl ZInit { .run( Process::new(&cfg.test, &cfg.dir, Some(cfg.env.clone())), log.clone(), + name.as_ref(), ) .await?; @@ -596,6 +598,14 @@ impl ZInit { config::Log::None => Log::None, config::Log::Stdout => Log::Stdout, config::Log::Ring => Log::Ring(name.clone()), + config::Log::File => { + if let Some(log_file) = &config.log_file { + Log::File(log_file.clone().into()) + } else { + error!("log_file is not specified for service '{}'", name); + Log::None + } + } }; let mut service = input.write().await; @@ -614,6 +624,7 @@ impl ZInit { .run( Process::new(&config.exec, &config.dir, Some(config.env.clone())), log.clone(), + &name, ) .await;