diff --git a/AGENTS.md b/AGENTS.md index d6350ac..8506157 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,14 +9,14 @@ - **Clarity over cleverness.** Be concise, but favour explicit over terse or obscure idioms. Prefer code that's easy to follow. - **Use functions and composition.** Avoid repetition by extracting reusable - logic. Prefer generators or comprehensions, and declarative code to imperative - repetition when readable. + logic. Prefer generators, comprehensions, and declarative code to + imperative repetition when readable. - **Small, meaningful functions.** Functions must be small, clear in purpose, single responsibility, and obey command/query segregation. - **Clear commit messages.** Commit messages should be descriptive, explaining what was changed and why. - **Name things precisely.** Use clear, descriptive variable and function names. - For booleans, prefer names with `is`, `has`, or `should`. + For booleans, prefer names with `is`, `has`, `should`. - **Structure logically.** Each file should encapsulate a coherent module. Group related code (e.g., models + utilities + fixtures) close together. - **Group by feature, not layer.** Colocate views, logic, fixtures, and helpers @@ -25,12 +25,13 @@ ("-ize" / "-yse" / "-our") spelling and grammar, with the exception of references to external APIs. - **Illustrate with clear examples.** Function documentation must include clear - examples demonstrating the usage and outcome of the function. Test documentation - should omit examples where the example serves only to reiterate the test logic. -- **Keep file size managable.** No single code file may be longer than 400 lines. - Long switch statements or dispatch tables should be broken up by feature and - constituents colocated with targets. Large blocks of test data should be moved - to external data files. + examples demonstrating the usage and outcome of the function. Test + documentation should omit examples where the example serves only to reiterate + the test logic. +- **Keep file size manageable.** No single code file may be longer than 400 + lines. Long switch statements or dispatch tables should be broken up by + feature and constituents colocated with targets. Large blocks of test data + should be moved to external data files. ## Documentation Maintenance @@ -42,8 +43,8 @@ relevant file(s) in the `docs/` directory to reflect the latest state. **Ensure the documentation remains accurate and current.** - Documentation must use en-GB-oxendict ("-ize" / "-yse" / "-our") spelling - and grammar. (EXCEPTION: the naming of the "LICENSE" file, which - is to be left unchanged for community consistency.) + and grammar. (EXCEPTION: the naming of the "LICENSE" file, which is to be + left unchanged for community consistency.) ## Change Quality & Committing @@ -153,19 +154,19 @@ project: specified in `Cargo.toml` must use SemVer-compatible caret requirements (e.g., `some-crate = "1.2.3"`). This is Cargo's default and allows for safe, non-breaking updates to minor and patch versions while preventing breaking - changes from new major versions. This approach is critical for ensuring - build stability and reproducibility. + changes from new major versions. This approach is critical for ensuring build + stability and reproducibility. - **Prohibit unstable version specifiers.** The use of wildcard (`*`) or - open-ended inequality (`>=`) version requirements is strictly forbidden - as they introduce unacceptable risk and unpredictability. Tilde requirements + open-ended inequality (`>=`) version requirements are strictly forbidden, as + they introduce unacceptable risk and unpredictability. Tilde requirements (`~`) should only be used where a dependency must be locked to patch-level updates for a specific, documented reason. ### Error Handling - **Prefer semantic error enums**. Derive `std::error::Error` (via the - `thiserror` crate) for any condition the caller might inspect, retry, or - map to an HTTP status. + `thiserror` crate) for any condition the caller might inspect, retry, or map + to an HTTP status. - **Use an *opaque* error only at the app boundary**. Use `eyre::Report` for human-readable logs; these should not be exposed in public APIs. - **Never export the opaque type from a library**. Convert to domain enums at diff --git a/Cargo.lock b/Cargo.lock index f456bed..6059b6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -561,6 +561,22 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +[[package]] +name = "figment" +version = "0.10.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3" +dependencies = [ + "atomic", + "parking_lot", + "pear", + "serde", + "tempfile", + "toml", + "uncased", + "version_check", +] + [[package]] name = "filetime" version = "0.2.25" @@ -3204,6 +3220,15 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" diff --git a/crates/comenqd/src/daemon.rs b/crates/comenqd/src/daemon.rs index 4a9b226..b13983a 100644 --- a/crates/comenqd/src/daemon.rs +++ b/crates/comenqd/src/daemon.rs @@ -7,11 +7,12 @@ use crate::config::Config; use anyhow::Result; use comenq_lib::CommentRequest; use octocrab::Octocrab; -use std::fs; +use std::fs as stdfs; use std::os::unix::fs::PermissionsExt; use std::path::Path; use std::sync::Arc; use std::time::Duration; +use tokio::fs; use tokio::io::AsyncReadExt; use tokio::net::{UnixListener, UnixStream}; use yaque::{Receiver, Sender, channel}; @@ -23,16 +24,23 @@ fn build_octocrab(token: &str) -> Result { } fn prepare_listener(path: &Path) -> Result { - if fs::metadata(path).is_ok() { - fs::remove_file(path)?; + if stdfs::metadata(path).is_ok() { + stdfs::remove_file(path)?; } let listener = UnixListener::bind(path)?; - fs::set_permissions(path, fs::Permissions::from_mode(0o660))?; + stdfs::set_permissions(path, stdfs::Permissions::from_mode(0o660))?; Ok(listener) } +async fn ensure_queue_dir(path: &Path) -> Result<()> { + fs::create_dir_all(path).await?; + Ok(()) +} + /// Start the daemon with the provided configuration. pub async fn run(config: Config) -> Result<()> { + ensure_queue_dir(&config.queue_path).await?; + tracing::info!(queue = %config.queue_path.display(), "Queue directory prepared"); let octocrab = Arc::new(build_octocrab(&config.github_token)?); let (tx, rx) = channel(&config.queue_path)?; let cfg = Arc::new(config); @@ -96,3 +104,47 @@ async fn run_worker(config: Arc, mut rx: Receiver, octocrab: Arc timeout { + break; + } + sleep(Duration::from_millis(10)).await; + } + + handle.abort(); + assert!(cfg.queue_path.is_dir(), "queue directory not created"); + } +} diff --git a/crates/comenqd/src/logging.rs b/crates/comenqd/src/logging.rs new file mode 100644 index 0000000..14162c0 --- /dev/null +++ b/crates/comenqd/src/logging.rs @@ -0,0 +1,76 @@ +//! Logging utilities for the daemon. +//! +//! Initializes structured logging using `tracing` and +//! `tracing-subscriber`, reading filter settings from the `RUST_LOG` +//! environment variable. + +use tracing_subscriber::fmt::MakeWriter; +use tracing_subscriber::{EnvFilter, fmt}; + +/// Initialize the global tracing subscriber. +pub fn init() { + init_with_writer(fmt::writer::BoxMakeWriter::new(std::io::stdout)); +} + +/// Initialize logging with a custom writer. +pub fn init_with_writer(writer: W) +where + W: for<'a> MakeWriter<'a> + Send + Sync + 'static, +{ + fmt() + .with_env_filter(EnvFilter::from_default_env()) + .with_writer(writer) + .json() + .init(); +} + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::{Arc, Mutex}; + use tracing::info; + + #[derive(Clone)] + struct BufMakeWriter { + buf: Arc>>, + } + + impl<'a> MakeWriter<'a> for BufMakeWriter { + type Writer = BufWriter; + + fn make_writer(&'a self) -> Self::Writer { + BufWriter { + buf: self.buf.clone(), + } + } + } + + struct BufWriter { + buf: Arc>>, + } + + impl std::io::Write for BufWriter { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.buf + .lock() + .expect("Failed to lock log buffer") + .extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + Ok(()) + } + } + + #[test] + fn init_logging() { + let buf = Arc::new(Mutex::new(Vec::new())); + std::env::set_var("RUST_LOG", "info"); + init_with_writer(BufMakeWriter { buf: buf.clone() }); + info!("captured"); + let output = String::from_utf8(buf.lock().expect("Failed to lock log buffer").clone()) + .expect("Captured output is not valid UTF-8"); + assert!(output.contains("captured")); + } +} diff --git a/crates/comenqd/src/main.rs b/crates/comenqd/src/main.rs index 03e11b7..2caa17c 100644 --- a/crates/comenqd/src/main.rs +++ b/crates/comenqd/src/main.rs @@ -4,6 +4,8 @@ use tracing::info; +mod logging; + mod config; mod daemon; use config::Config; @@ -11,7 +13,7 @@ use daemon::run; #[tokio::main] async fn main() -> anyhow::Result<()> { - tracing_subscriber::fmt::init(); + logging::init(); let cfg = Config::load()?; info!(socket = ?cfg.socket_path, queue = ?cfg.queue_path, "Comenqd daemon started"); run(cfg).await diff --git a/docs/comenq-design.md b/docs/comenq-design.md index 135ffc3..7b72ec8 100644 --- a/docs/comenq-design.md +++ b/docs/comenq-design.md @@ -1013,7 +1013,10 @@ async fn run_worker(config: Arc, mut rx: Receiver, octoc The repository initialises the workspace with `comenq-lib` at the root and two binary crates under `crates/`. `CommentRequest` resides in the library and derives both `Serialize` and `Deserialize`. The daemon now spawns a Unix -listener and queue worker as described above. +listener and queue worker as described above. Structured logging is initialised +using `tracing_subscriber` with JSON output controlled by the `RUST_LOG` +environment variable. The queue directory is created asynchronously on start if +it does not already exist before `yaque` opens it. ## Works cited diff --git a/docs/roadmap.md b/docs/roadmap.md index 7d321d8..a632f45 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -34,13 +34,13 @@ (`/etc/comenqd/config.toml`) for parameters like `github_token`, `socket_path`, and `queue_path`. -- [ ] Set up structured logging using the `tracing` and `tracing-subscriber` +- [x] Set up structured logging using the `tracing` and `tracing-subscriber` crates. -- [ ] Initialize the `yaque` persistent queue at the path specified in the +- [x] Initialize the `yaque` persistent queue at the path specified in the configuration. -- [ ] Structure the daemon's `main` function to spawn the two primary, +- [x] Structure the daemon's `main` function to spawn the two primary, long-running `tokio` tasks: the UDS listener and the queue worker. ## Milestone 4: `comenqd` Daemon — UDS Listener Task