Skip to main content

aperture_cli/cli/
tracing_init.rs

1//! Tracing/logging initialization for the CLI.
2
3use tracing_subscriber::EnvFilter;
4
5/// Wrapper type to write logs to file or stderr.
6struct FileOrStderr {
7    file: Option<std::sync::Mutex<std::fs::File>>,
8}
9
10impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for FileOrStderr {
11    type Writer = Box<dyn std::io::Write + 'a>;
12
13    fn make_writer(&'a self) -> Self::Writer {
14        self.file
15            .as_ref()
16            .and_then(|mutex| mutex.lock().ok())
17            .and_then(|file| file.try_clone().ok())
18            .map_or_else(
19                || Box::new(std::io::stderr()) as Self::Writer,
20                |cloned| Box::new(cloned) as Self::Writer,
21            )
22    }
23}
24
25/// Initialize tracing-subscriber for request/response logging.
26pub fn init_tracing(verbosity: u8) {
27    use std::fs::OpenOptions;
28    use std::sync::Mutex;
29    use tracing_subscriber::fmt::format::FmtSpan;
30    use tracing_subscriber::layer::SubscriberExt;
31    use tracing_subscriber::util::SubscriberInitExt;
32
33    let log_level_str = if verbosity > 0 {
34        match verbosity {
35            1 => "debug".to_string(),
36            _ => "trace".to_string(),
37        }
38    } else {
39        std::env::var("APERTURE_LOG").unwrap_or_else(|_| "warn".to_string())
40    };
41
42    let env_filter = EnvFilter::try_new(&log_level_str)
43        .or_else(|_| EnvFilter::try_new("error"))
44        .unwrap_or_else(|_| EnvFilter::new("error"));
45
46    let log_format = std::env::var("APERTURE_LOG_FORMAT")
47        .map_or_else(|_| "text".to_string(), |s| s.to_lowercase());
48
49    if log_format != "json" && log_format != "text" {
50        // Tracing is not yet initialized; eprintln! is the only output channel available.
51        // ast-grep-ignore: no-println
52        eprintln!(
53            "Warning: Unrecognized APERTURE_LOG_FORMAT '{log_format}'. Valid values: 'json', 'text'. Using 'text'."
54        );
55    }
56
57    let writer = std::env::var("APERTURE_LOG_FILE").ok().map_or_else(
58        || FileOrStderr { file: None },
59        |path| match OpenOptions::new().create(true).append(true).open(&path) {
60            Ok(file) => FileOrStderr {
61                file: Some(Mutex::new(file)),
62            },
63            Err(e) => {
64                // Tracing is not yet initialized; eprintln! is the only output channel available.
65                // ast-grep-ignore: no-println
66                eprintln!("Warning: Could not open log file '{path}': {e}. Using stderr.");
67                FileOrStderr { file: None }
68            }
69        },
70    );
71
72    if log_format == "json" {
73        let json_layer = tracing_subscriber::fmt::layer()
74            .json()
75            .with_span_list(false)
76            .with_target(true)
77            .with_thread_ids(false)
78            .with_line_number(true)
79            .with_writer(writer);
80        tracing_subscriber::registry()
81            .with(env_filter)
82            .with(json_layer)
83            .init();
84    } else {
85        let fmt_layer = tracing_subscriber::fmt::layer()
86            .pretty()
87            .with_span_events(FmtSpan::CLOSE)
88            .with_target(false)
89            .with_thread_ids(false)
90            .with_line_number(false)
91            .with_writer(writer);
92        tracing_subscriber::registry()
93            .with(env_filter)
94            .with(fmt_layer)
95            .init();
96    }
97}