@@ -9,28 +9,39 @@ use chrono::{Local, Utc};
99use log:: LevelFilter ;
1010use std:: fs:: { self , File } ;
1111use std:: io:: Write ;
12- use std:: path:: PathBuf ;
12+ use std:: path:: { Path , PathBuf } ;
1313use 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
6778struct FileLogger {
6879 file : Mutex < File > ,
69- level : LevelFilter ,
80+ file_level : LevelFilter ,
81+ stderr_level : LevelFilter ,
7082}
7183
7284impl 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) ]
197225mod 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}
0 commit comments