Skip to content

Commit 64e9709

Browse files
fix(logging): always capture debug logs to file while preserving console verbosity (#430)
* feat(logging): add configurable log output dir and debug pipeline ado-aw flags Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/78e8e09d-bd5a-43f7-83e6-6adf4097b88c Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * fix(logging): remove unused wrapper fns and keep override-based path resolution Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/78e8e09d-bd5a-43f7-83e6-6adf4097b88c Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * fix(logging): always write debug-level logs to file while keeping stderr verbosity Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/fc1c4059-6c5b-40a3-b88d-c0a5baad0498 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> * fix(compile): stop injecting ado-aw debug flags for --debug-pipeline Agent-Logs-Url: https://github.com/githubnext/ado-aw/sessions/b9291c25-ff23-4838-a2d9-e2a050dd7aa4 Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jamesadevine <4742697+jamesadevine@users.noreply.github.com>
1 parent 018856e commit 64e9709

6 files changed

Lines changed: 118 additions & 57 deletions

File tree

docs/cli.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ _Part of the [ado-aw documentation](../AGENTS.md)._
44

55
## CLI Commands
66

7-
Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logging), `--debug, -d` (enable debug-level logging, implies verbose)
7+
Global flags (apply to all subcommands): `--verbose, -v` (enable info-level logging), `--debug, -d` (enable debug-level logging, implies verbose), `--log-output-dir <path>` (write ado-aw logs to a specific directory; overrides `ADO_AW_LOG_DIR`)
88

99
- `init` - Initialize a repository for AI-first agentic pipeline authoring
1010
- `--path <path>` - Target directory (defaults to current directory)

src/compile/common.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -709,7 +709,7 @@ pub fn generate_integrity_check(skip: bool) -> String {
709709
/// - `{{ verify_mcp_backends }}`: full pipeline step that probes each MCPG
710710
/// backend with MCP initialize + tools/list
711711
///
712-
/// When `debug` is `false`, both markers resolve to empty strings.
712+
/// When `debug` is `false`, debug markers resolve to empty strings.
713713
pub fn generate_debug_pipeline_replacements(debug: bool) -> Vec<(String, String)> {
714714
if !debug {
715715
return vec![

src/data/1es-base.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -395,8 +395,9 @@ extends:
395395
if [ -d "{{ engine_log_dir }}" ]; then
396396
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
397397
fi
398-
if [ -d ~/.ado-aw/logs ]; then
399-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
398+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
399+
if [ -d "$ADO_AW_LOG_DIR" ]; then
400+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
400401
fi
401402
if [ -d /tmp/gh-aw/mcp-logs ]; then
402403
mkdir -p "$(Agent.TempDirectory)/staging/logs/mcpg"
@@ -593,9 +594,10 @@ extends:
593594
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot"
594595
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true
595596
fi
596-
if [ -d ~/.ado-aw/logs ]; then
597+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
598+
if [ -d "$ADO_AW_LOG_DIR" ]; then
597599
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw"
598-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true
600+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true
599601
fi
600602
echo "Logs copied to $(Agent.TempDirectory)/analyzed_outputs/logs"
601603
ls -laR "$(Agent.TempDirectory)/analyzed_outputs/logs" 2>/dev/null || echo "No logs found"
@@ -674,9 +676,10 @@ extends:
674676
mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot"
675677
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true
676678
fi
677-
if [ -d ~/.ado-aw/logs ]; then
679+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
680+
if [ -d "$ADO_AW_LOG_DIR" ]; then
678681
mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw"
679-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true
682+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true
680683
fi
681684
echo "Logs copied to $(Agent.TempDirectory)/staging/logs"
682685
ls -laR "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found"

src/data/base.yml

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -366,8 +366,9 @@ jobs:
366366
if [ -d "{{ engine_log_dir }}" ]; then
367367
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
368368
fi
369-
if [ -d ~/.ado-aw/logs ]; then
370-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
369+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
370+
if [ -d "$ADO_AW_LOG_DIR" ]; then
371+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/" 2>/dev/null || true
371372
fi
372373
if [ -d /tmp/gh-aw/mcp-logs ]; then
373374
mkdir -p "$(Agent.TempDirectory)/staging/logs/mcpg"
@@ -562,9 +563,10 @@ jobs:
562563
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot"
563564
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/copilot/" 2>/dev/null || true
564565
fi
565-
if [ -d ~/.ado-aw/logs ]; then
566+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
567+
if [ -d "$ADO_AW_LOG_DIR" ]; then
566568
mkdir -p "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw"
567-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true
569+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/analyzed_outputs/logs/ado-aw/" 2>/dev/null || true
568570
fi
569571
echo "Logs copied to $(Agent.TempDirectory)/analyzed_outputs/logs"
570572
ls -laR "$(Agent.TempDirectory)/analyzed_outputs/logs" 2>/dev/null || echo "No logs found"
@@ -642,9 +644,10 @@ jobs:
642644
mkdir -p "$(Agent.TempDirectory)/staging/logs/copilot"
643645
cp -r "{{ engine_log_dir }}"/* "$(Agent.TempDirectory)/staging/logs/copilot/" 2>/dev/null || true
644646
fi
645-
if [ -d ~/.ado-aw/logs ]; then
647+
ADO_AW_LOG_DIR="${ADO_AW_LOG_DIR:-$HOME/.ado-aw/logs}"
648+
if [ -d "$ADO_AW_LOG_DIR" ]; then
646649
mkdir -p "$(Agent.TempDirectory)/staging/logs/ado-aw"
647-
cp -r ~/.ado-aw/logs/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true
650+
cp -r "$ADO_AW_LOG_DIR"/* "$(Agent.TempDirectory)/staging/logs/ado-aw/" 2>/dev/null || true
648651
fi
649652
echo "Logs copied to $(Agent.TempDirectory)/staging/logs"
650653
ls -laR "$(Agent.TempDirectory)/staging/logs" 2>/dev/null || echo "No logs found"

src/logging.rs

Lines changed: 88 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,39 @@ use chrono::{Local, Utc};
99
use log::LevelFilter;
1010
use std::fs::{self, File};
1111
use std::io::Write;
12-
use std::path::PathBuf;
12+
use std::path::{Path, PathBuf};
1313
use std::sync::Mutex;
1414

15-
/// Get the standard log directory path
15+
const FILE_LOG_LEVEL: LevelFilter = LevelFilter::Debug;
16+
17+
/// Resolve log directory, optionally overriding with a CLI-provided path.
1618
///
17-
/// Returns `$HOME/.ado-aw/logs/` on Unix/macOS
18-
/// Returns `%USERPROFILE%\.ado-aw\logs\` on Windows
19-
pub fn log_directory() -> Result<PathBuf> {
19+
/// Resolution order:
20+
/// 1. CLI override (`--log-output-dir`)
21+
/// 2. `ADO_AW_LOG_DIR` env var
22+
/// 3. Default (`$HOME/.ado-aw/logs` or `%USERPROFILE%\.ado-aw\logs`)
23+
fn log_directory(output_dir_override: Option<&Path>) -> Result<PathBuf> {
24+
if let Some(path) = output_dir_override {
25+
return Ok(path.to_path_buf());
26+
}
27+
if let Ok(from_env) = std::env::var("ADO_AW_LOG_DIR") {
28+
let trimmed = from_env.trim();
29+
if !trimmed.is_empty() {
30+
return Ok(PathBuf::from(trimmed));
31+
}
32+
}
2033
let home = dirs::home_dir().context("Could not determine home directory")?;
2134
Ok(home.join(".ado-aw").join("logs"))
2235
}
2336

24-
/// Get the path for today's log file
25-
pub fn daily_log_path() -> Result<PathBuf> {
26-
let log_dir = log_directory()?;
37+
fn daily_log_path_with_override(output_dir_override: Option<&Path>) -> Result<PathBuf> {
38+
let log_dir = log_directory(output_dir_override)?;
2739
let date = Local::now().format("%Y-%m-%d");
2840
Ok(log_dir.join(format!("{}.log", date)))
2941
}
3042

31-
/// Ensure the log directory exists
32-
pub fn ensure_log_directory() -> Result<PathBuf> {
33-
let log_dir = log_directory()?;
43+
fn ensure_log_directory_with_override(output_dir_override: Option<&Path>) -> Result<PathBuf> {
44+
let log_dir = log_directory(output_dir_override)?;
3445
fs::create_dir_all(&log_dir).context("Failed to create log directory")?;
3546
Ok(log_dir)
3647
}
@@ -66,12 +77,13 @@ fn build_session_marker(command_name: &str) -> String {
6677
/// A simple file logger that implements log::Log
6778
struct FileLogger {
6879
file: Mutex<File>,
69-
level: LevelFilter,
80+
file_level: LevelFilter,
81+
stderr_level: LevelFilter,
7082
}
7183

7284
impl log::Log for FileLogger {
7385
fn enabled(&self, metadata: &log::Metadata) -> bool {
74-
metadata.level() <= self.level
86+
metadata.level() <= self.file_level || metadata.level() <= self.stderr_level
7587
}
7688

7789
fn log(&self, record: &log::Record) {
@@ -85,14 +97,18 @@ impl log::Log for FileLogger {
8597
record.args()
8698
);
8799

88-
// Write to file
89-
if let Ok(mut file) = self.file.lock() {
90-
let _ = file.write_all(message.as_bytes());
91-
let _ = file.flush();
100+
// Write to file (always capture debug+)
101+
if record.level() <= self.file_level {
102+
if let Ok(mut file) = self.file.lock() {
103+
let _ = file.write_all(message.as_bytes());
104+
let _ = file.flush();
105+
}
92106
}
93107

94-
// Also write to stderr for immediate visibility
95-
eprint!("{}", message);
108+
// Write to stderr according to selected runtime verbosity
109+
if record.level() <= self.stderr_level {
110+
eprint!("{}", message);
111+
}
96112
}
97113
}
98114

@@ -110,13 +126,18 @@ impl log::Log for FileLogger {
110126
///
111127
/// # Arguments
112128
/// * `command_name` - Name of the command (included in session marker)
113-
/// * `level` - Minimum log level to capture
129+
/// * `stderr_level` - Runtime verbosity for stderr output
130+
/// * `output_dir_override` - Optional directory override for log output
114131
///
115132
/// # Returns
116133
/// Path to the log file, or error if initialization failed
117-
pub fn init_file_logging(command_name: &str, level: LevelFilter) -> Result<PathBuf> {
118-
ensure_log_directory()?;
119-
let log_path = daily_log_path()?;
134+
pub fn init_file_logging(
135+
command_name: &str,
136+
stderr_level: LevelFilter,
137+
output_dir_override: Option<&Path>,
138+
) -> Result<PathBuf> {
139+
ensure_log_directory_with_override(output_dir_override)?;
140+
let log_path = daily_log_path_with_override(output_dir_override)?;
120141

121142
// Open log file in append mode
122143
let file = fs::OpenOptions::new()
@@ -134,11 +155,12 @@ pub fn init_file_logging(command_name: &str, level: LevelFilter) -> Result<PathB
134155

135156
let logger = FileLogger {
136157
file: Mutex::new(file),
137-
level,
158+
file_level: FILE_LOG_LEVEL,
159+
stderr_level,
138160
};
139161

140162
log::set_boxed_logger(Box::new(logger))
141-
.map(|()| log::set_max_level(level))
163+
.map(|()| log::set_max_level(FILE_LOG_LEVEL.max(stderr_level)))
142164
.context("Failed to set logger")?;
143165

144166
Ok(log_path)
@@ -152,23 +174,29 @@ pub fn init_file_logging(command_name: &str, level: LevelFilter) -> Result<PathB
152174
/// * `command_name` - Name of the command for the session marker
153175
/// * `debug` - Enable debug level logging
154176
/// * `verbose` - Enable info level logging (ignored if debug is true)
177+
/// * `output_dir_override` - Optional directory override for log output
155178
///
156179
/// # Returns
157180
/// Path to the log file if file logging was initialized
158-
pub fn init_logging(command_name: &str, debug: bool, verbose: bool) -> Option<PathBuf> {
159-
let level = if debug {
181+
fn selected_stderr_level(debug: bool, verbose: bool, rust_log_set: bool) -> LevelFilter {
182+
if debug {
160183
LevelFilter::Debug
161-
} else if verbose {
162-
LevelFilter::Info
163-
} else if std::env::var("RUST_LOG").is_ok() {
164-
// If RUST_LOG is set, use Info as minimum for file logging
184+
} else if verbose || rust_log_set {
165185
LevelFilter::Info
166186
} else {
167-
// Default: only warnings and errors
168187
LevelFilter::Warn
169-
};
188+
}
189+
}
170190

171-
match init_file_logging(command_name, level) {
191+
pub fn init_logging(
192+
command_name: &str,
193+
debug: bool,
194+
verbose: bool,
195+
output_dir_override: Option<&Path>,
196+
) -> Option<PathBuf> {
197+
let stderr_level = selected_stderr_level(debug, verbose, std::env::var("RUST_LOG").is_ok());
198+
199+
match init_file_logging(command_name, stderr_level, output_dir_override) {
172200
Ok(path) => {
173201
log::debug!("Logging to: {}", path.display());
174202
Some(path)
@@ -179,12 +207,12 @@ pub fn init_logging(command_name: &str, debug: bool, verbose: bool) -> Option<Pa
179207

180208
// Use env_logger as fallback
181209
let mut builder = env_logger::Builder::new();
182-
if debug {
183-
builder.filter_level(LevelFilter::Debug);
184-
} else if verbose {
185-
builder.filter_level(LevelFilter::Info);
210+
if debug || verbose {
211+
builder.filter_level(stderr_level);
186212
} else if let Ok(rust_log) = std::env::var("RUST_LOG") {
187213
builder.parse_filters(&rust_log);
214+
} else {
215+
builder.filter_level(stderr_level);
188216
}
189217
let _ = builder.try_init();
190218

@@ -196,18 +224,19 @@ pub fn init_logging(command_name: &str, debug: bool, verbose: bool) -> Option<Pa
196224
#[cfg(test)]
197225
mod tests {
198226
use super::*;
227+
use tempfile::tempdir;
199228

200229
#[test]
201230
fn test_log_directory() {
202-
let dir = log_directory().unwrap();
231+
let dir = log_directory(None).unwrap();
203232
assert!(
204233
dir.ends_with(".ado-aw/logs") || dir.ends_with(".ado-aw\\logs")
205234
);
206235
}
207236

208237
#[test]
209238
fn test_daily_log_path() {
210-
let path = daily_log_path().unwrap();
239+
let path = daily_log_path_with_override(None).unwrap();
211240
let filename = path.file_name().unwrap().to_string_lossy();
212241
// Should be YYYY-MM-DD.log format
213242
assert!(filename.ends_with(".log"));
@@ -216,15 +245,33 @@ mod tests {
216245

217246
#[test]
218247
fn test_ensure_log_directory() {
219-
let dir = ensure_log_directory().unwrap();
248+
let dir = ensure_log_directory_with_override(None).unwrap();
220249
assert!(dir.exists());
221250
}
222251

252+
#[test]
253+
fn test_log_directory_override() {
254+
let temp = tempdir().unwrap();
255+
let dir = log_directory(Some(temp.path())).unwrap();
256+
assert_eq!(dir, temp.path());
257+
}
258+
223259
#[test]
224260
fn test_build_session_marker() {
225261
let marker = build_session_marker("test-command");
226262
assert!(marker.starts_with("=== ["));
227263
assert!(marker.contains("COMMAND=test-command"));
228264
assert!(marker.ends_with(" ==="));
229265
}
266+
267+
#[test]
268+
fn test_selected_stderr_level() {
269+
assert_eq!(
270+
selected_stderr_level(true, false, false),
271+
LevelFilter::Debug
272+
);
273+
assert_eq!(selected_stderr_level(false, true, false), LevelFilter::Info);
274+
assert_eq!(selected_stderr_level(false, false, true), LevelFilter::Info);
275+
assert_eq!(selected_stderr_level(false, false, false), LevelFilter::Warn);
276+
}
230277
}

src/main.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ struct Args {
143143
/// Enable debug logging (debug level, implies verbose)
144144
#[arg(short, long, global = true)]
145145
debug: bool,
146+
/// Output directory for ado-aw log files (overrides ADO_AW_LOG_DIR)
147+
#[arg(long, global = true)]
148+
log_output_dir: Option<PathBuf>,
146149
#[command(subcommand)]
147150
command: Option<Commands>,
148151
}
@@ -337,8 +340,13 @@ async fn main() -> Result<()> {
337340
None => "ado-aw",
338341
};
339342

340-
// Initialize file-based logging to $HOME/.ado-aw/logs/{command}.log
341-
let _log_path = logging::init_logging(command_name, args.debug, args.verbose);
343+
// Initialize file-based logging to a daily log file.
344+
let _log_path = logging::init_logging(
345+
command_name,
346+
args.debug,
347+
args.verbose,
348+
args.log_output_dir.as_deref(),
349+
);
342350

343351
let Some(command) = args.command else {
344352
println!("No subcommand was used. Try `compile <path>`");

0 commit comments

Comments
 (0)