diff --git a/docs/cli.md b/docs/cli.md index c4e430f..bb272c6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -4,7 +4,7 @@ _Part of the [ado-aw documentation](../AGENTS.md)._ ## CLI Commands -Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logging), `--debug, -d` (enable debug-level logging, implies verbose) +Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logging), `--debug, -d` (enable debug-level logging, implies verbose), `--log-output-dir ` (write ado-aw logs to a specific directory; overrides `ADO_AW_LOG_DIR`) - `init` - Initialize a repository for AI-first agentic pipeline authoring - `--path ` - Target directory (defaults to current directory) diff --git a/src/compile/common.rs b/src/compile/common.rs index 6e2767e..207c5d5 100644 --- a/src/compile/common.rs +++ b/src/compile/common.rs @@ -709,7 +709,7 @@ pub fn generate_integrity_check(skip: bool) -> String { /// - `{{ verify_mcp_backends }}`: full pipeline step that probes each MCPG /// backend with MCP initialize + tools/list /// -/// When `debug` is `false`, both markers resolve to empty strings. +/// When `debug` is `false`, debug markers resolve to empty strings. pub fn generate_debug_pipeline_replacements(debug: bool) -> Vec<(String, String)> { if !debug { return vec![ diff --git a/src/data/1es-base.yml b/src/data/1es-base.yml index e040703..6cc2069 100644 --- a/src/data/1es-base.yml +++ b/src/data/1es-base.yml @@ -395,8 +395,9 @@ extends: if [ -d "{{ engine_log_dir }}" ]; then cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi if [ -d /tmp/gh-aw/mcp-logs ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/mcpg" @@ -593,9 +594,10 @@ extends: mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true fi echo "Logs copied to $(Agent.TempDirectory)/analyzed_outputs/logs" ls -laR "$(Agent.TempDirectory)/analyzed_outputs/logs" 2>/dev/null || echo "No logs found" @@ -674,9 +676,10 @@ extends: mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw" - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true fi echo "Logs copied to $(Agent.TempDirectory)/staging/logs" ls -laR "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found" diff --git a/src/data/base.yml b/src/data/base.yml index 24a9ba8..a423a1e 100644 --- a/src/data/base.yml +++ b/src/data/base.yml @@ -366,8 +366,9 @@ jobs: if [ -d "{{ engine_log_dir }}" ]; then cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true fi if [ -d /tmp/gh-aw/mcp-logs ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/mcpg" @@ -562,9 +563,10 @@ jobs: mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot" cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw" - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true fi echo "Logs copied to $(Agent.TempDirectory)/analyzed_outputs/logs" ls -laR "$(Agent.TempDirectory)/analyzed_outputs/logs" 2>/dev/null || echo "No logs found" @@ -642,9 +644,10 @@ jobs: mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot" cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true fi - if [ -d ~/.ado-aw/logs ]; then + ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}" + if [ -d "$ADO_AW_LOG_DIR" ]; then mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw" - cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true + cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true fi echo "Logs copied to $(Agent.TempDirectory)/staging/logs" ls -laR "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found" diff --git a/src/logging.rs b/src/logging.rs index 149855f..710c2a8 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -9,28 +9,39 @@ use chrono::{Local, Utc}; use log::LevelFilter; use std::fs::{self, File}; use std::io::Write; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::sync::Mutex; -/// Get the standard log directory path +const FILE_LOG_LEVEL: LevelFilter = LevelFilter::Debug; + +/// Resolve log directory, optionally overriding with a CLI-provided path. /// -/// Returns `$HOME/.ado-aw/logs/` on Unix/macOS -/// Returns `%USERPROFILE%\.ado-aw\logs\` on Windows -pub fn log_directory() -> Result { +/// Resolution order: +/// 1. CLI override (`--log-output-dir`) +/// 2. `ADO_AW_LOG_DIR` env var +/// 3. Default (`$HOME/.ado-aw/logs` or `%USERPROFILE%\.ado-aw\logs`) +fn log_directory(output_dir_override: Option<&Path>) -> Result { + if let Some(path) = output_dir_override { + return Ok(path.to_path_buf()); + } + if let Ok(from_env) = std::env::var("ADO_AW_LOG_DIR") { + let trimmed = from_env.trim(); + if !trimmed.is_empty() { + return Ok(PathBuf::from(trimmed)); + } + } let home = dirs::home_dir().context("Could not determine home directory")?; Ok(home.join(".ado-aw").join("logs")) } -/// Get the path for today's log file -pub fn daily_log_path() -> Result { - let log_dir = log_directory()?; +fn daily_log_path_with_override(output_dir_override: Option<&Path>) -> Result { + let log_dir = log_directory(output_dir_override)?; let date = Local::now().format("%Y-%m-%d"); Ok(log_dir.join(format!("{}.log", date))) } -/// Ensure the log directory exists -pub fn ensure_log_directory() -> Result { - let log_dir = log_directory()?; +fn ensure_log_directory_with_override(output_dir_override: Option<&Path>) -> Result { + let log_dir = log_directory(output_dir_override)?; fs::create_dir_all(&log_dir).context("Failed to create log directory")?; Ok(log_dir) } @@ -66,12 +77,13 @@ fn build_session_marker(command_name: &str) -> String { /// A simple file logger that implements log::Log struct FileLogger { file: Mutex, - level: LevelFilter, + file_level: LevelFilter, + stderr_level: LevelFilter, } impl log::Log for FileLogger { fn enabled(&self, metadata: &log::Metadata) -> bool { - metadata.level() <= self.level + metadata.level() <= self.file_level || metadata.level() <= self.stderr_level } fn log(&self, record: &log::Record) { @@ -85,14 +97,18 @@ impl log::Log for FileLogger { record.args() ); - // Write to file - if let Ok(mut file) = self.file.lock() { - let _ = file.write_all(message.as_bytes()); - let _ = file.flush(); + // Write to file (always capture debug+) + if record.level() <= self.file_level { + if let Ok(mut file) = self.file.lock() { + let _ = file.write_all(message.as_bytes()); + let _ = file.flush(); + } } - // Also write to stderr for immediate visibility - eprint!("{}", message); + // Write to stderr according to selected runtime verbosity + if record.level() <= self.stderr_level { + eprint!("{}", message); + } } } @@ -110,13 +126,18 @@ impl log::Log for FileLogger { /// /// # Arguments /// * `command_name` - Name of the command (included in session marker) -/// * `level` - Minimum log level to capture +/// * `stderr_level` - Runtime verbosity for stderr output +/// * `output_dir_override` - Optional directory override for log output /// /// # Returns /// Path to the log file, or error if initialization failed -pub fn init_file_logging(command_name: &str, level: LevelFilter) -> Result { - ensure_log_directory()?; - let log_path = daily_log_path()?; +pub fn init_file_logging( + command_name: &str, + stderr_level: LevelFilter, + output_dir_override: Option<&Path>, +) -> Result { + ensure_log_directory_with_override(output_dir_override)?; + let log_path = daily_log_path_with_override(output_dir_override)?; // Open log file in append mode let file = fs::OpenOptions::new() @@ -134,11 +155,12 @@ pub fn init_file_logging(command_name: &str, level: LevelFilter) -> Result Result Option { - let level = if debug { +fn selected_stderr_level(debug: bool, verbose: bool, rust_log_set: bool) -> LevelFilter { + if debug { LevelFilter::Debug - } else if verbose { - LevelFilter::Info - } else if std::env::var("RUST_LOG").is_ok() { - // If RUST_LOG is set, use Info as minimum for file logging + } else if verbose || rust_log_set { LevelFilter::Info } else { - // Default: only warnings and errors LevelFilter::Warn - }; + } +} - match init_file_logging(command_name, level) { +pub fn init_logging( + command_name: &str, + debug: bool, + verbose: bool, + output_dir_override: Option<&Path>, +) -> Option { + let stderr_level = selected_stderr_level(debug, verbose, std::env::var("RUST_LOG").is_ok()); + + match init_file_logging(command_name, stderr_level, output_dir_override) { Ok(path) => { log::debug!("Logging to: {}", path.display()); Some(path) @@ -179,12 +207,12 @@ pub fn init_logging(command_name: &str, debug: bool, verbose: bool) -> Option Option, #[command(subcommand)] command: Option, } @@ -337,8 +340,13 @@ async fn main() -> Result<()> { None => "ado-aw", }; - // Initialize file-based logging to $HOME/.ado-aw/logs/{command}.log - let _log_path = logging::init_logging(command_name, args.debug, args.verbose); + // Initialize file-based logging to a daily log file. + let _log_path = logging::init_logging( + command_name, + args.debug, + args.verbose, + args.log_output_dir.as_deref(), + ); let Some(command) = args.command else { println!("No subcommand was used. Try `compile `");