Skip to main content

nntp_proxy/
logging.rs

1//! Centralized logging setup for stdout, local TUI capture, and optional debug files.
2
3use tracing_appender::non_blocking::{NonBlocking, WorkerGuard};
4use tracing_subscriber::Layer;
5use tracing_subscriber::layer::SubscriberExt;
6use tracing_subscriber::registry::LookupSpan;
7use tracing_subscriber::util::SubscriberInitExt;
8
9use crate::UiMode;
10
11#[cfg(feature = "tokio-console")]
12fn tokio_console_enabled() -> bool {
13    std::env::var_os("NNTP_PROXY_TOKIO_CONSOLE").is_some_and(|value| value != "0")
14}
15
16#[cfg(feature = "tokio-console")]
17fn tokio_console_layer<S>() -> Option<Box<dyn Layer<S> + Send + Sync + 'static>>
18where
19    S: tracing::Subscriber,
20    S: for<'span> LookupSpan<'span>,
21{
22    tokio_console_enabled().then(|| {
23        eprintln!(
24            "tokio-console enabled; console-subscriber environment controls its bind address"
25        );
26        console_subscriber::ConsoleLayer::builder()
27            .with_default_env()
28            .spawn()
29            .boxed()
30    })
31}
32
33#[cfg(not(feature = "tokio-console"))]
34fn tokio_console_layer<S>() -> Option<Box<dyn Layer<S> + Send + Sync + 'static>>
35where
36    S: tracing::Subscriber,
37    S: for<'span> LookupSpan<'span>,
38{
39    None
40}
41
42fn env_filter() -> tracing_subscriber::EnvFilter {
43    tracing_subscriber::EnvFilter::try_from_default_env()
44        .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"))
45}
46
47fn file_filter(file_level: &str) -> tracing_subscriber::EnvFilter {
48    tracing_subscriber::EnvFilter::new(file_level)
49}
50
51fn wants_debug_log_file(file_level: &str) -> bool {
52    file_level
53        .split(',')
54        .filter_map(|directive| directive.rsplit('=').next())
55        .map(str::trim)
56        .any(|level| level.eq_ignore_ascii_case("debug") || level.eq_ignore_ascii_case("trace"))
57}
58
59#[must_use]
60pub fn should_write_debug_log(ui_mode: UiMode, is_attached_tui: bool, file_level: &str) -> bool {
61    ui_mode == UiMode::Tui && !is_attached_tui && wants_debug_log_file(file_level)
62}
63
64#[must_use]
65pub const fn should_capture_in_memory_logs(
66    ui_mode: UiMode,
67    is_attached_tui: bool,
68    capture_headless_tui_buffer: bool,
69) -> bool {
70    match ui_mode {
71        UiMode::Headless => capture_headless_tui_buffer,
72        UiMode::Tui => !is_attached_tui,
73    }
74}
75
76fn debug_log_writer() -> (NonBlocking, WorkerGuard) {
77    let file_appender = tracing_appender::rolling::never(".", "debug.log");
78    tracing_appender::non_blocking(file_appender)
79}
80
81fn leak_guard(guard: WorkerGuard) {
82    // SAFETY: Intentionally leak the WorkerGuard to keep the file appender
83    // alive for the program lifetime. Drop would flush and close the writer.
84    std::mem::forget(guard);
85}
86
87fn init_headless_subscriber(capture_in_memory_logs: bool) -> Option<crate::tui::LogBuffer> {
88    let log_buffer = capture_in_memory_logs.then(crate::tui::LogBuffer::new);
89
90    let subscriber = tracing_subscriber::registry()
91        .with(
92            tracing_subscriber::fmt::layer()
93                .with_writer(|| std::io::LineWriter::new(std::io::stdout()))
94                .with_filter(env_filter()),
95        )
96        .with(tokio_console_layer());
97
98    match log_buffer.clone() {
99        Some(log_buffer) => subscriber
100            .with(
101                tracing_subscriber::fmt::layer()
102                    .with_writer(crate::tui::LogMakeWriter::new(log_buffer))
103                    .with_ansi(false)
104                    .with_target(false)
105                    .compact()
106                    .with_filter(env_filter()),
107            )
108            .init(),
109        None => subscriber.init(),
110    }
111
112    log_buffer
113}
114
115fn init_tui_subscriber(
116    file_level: &str,
117    capture_in_memory_logs: bool,
118    write_debug_log: bool,
119) -> Option<crate::tui::LogBuffer> {
120    match (capture_in_memory_logs, write_debug_log) {
121        (true, true) => {
122            let log_buffer = crate::tui::LogBuffer::new();
123            let (debug_log, guard) = debug_log_writer();
124            tracing_subscriber::registry()
125                .with(tokio_console_layer())
126                .with(
127                    tracing_subscriber::fmt::layer()
128                        .with_writer(crate::tui::LogMakeWriter::new(log_buffer.clone()))
129                        .with_ansi(false)
130                        .with_target(false)
131                        .compact()
132                        .with_filter(env_filter()),
133                )
134                .with(
135                    tracing_subscriber::fmt::layer()
136                        .with_writer(debug_log)
137                        .with_ansi(false)
138                        .with_filter(file_filter(file_level)),
139                )
140                .init();
141            leak_guard(guard);
142            Some(log_buffer)
143        }
144        (true, false) => {
145            let log_buffer = crate::tui::LogBuffer::new();
146            tracing_subscriber::registry()
147                .with(tokio_console_layer())
148                .with(
149                    tracing_subscriber::fmt::layer()
150                        .with_writer(crate::tui::LogMakeWriter::new(log_buffer.clone()))
151                        .with_ansi(false)
152                        .with_target(false)
153                        .compact()
154                        .with_filter(env_filter()),
155                )
156                .init();
157            Some(log_buffer)
158        }
159        (false, true) => {
160            let (debug_log, guard) = debug_log_writer();
161            tracing_subscriber::registry()
162                .with(tokio_console_layer())
163                .with(
164                    tracing_subscriber::fmt::layer()
165                        .with_writer(debug_log)
166                        .with_ansi(false)
167                        .with_filter(file_filter(file_level)),
168                )
169                .init();
170            leak_guard(guard);
171            None
172        }
173        (false, false) => {
174            tracing_subscriber::registry()
175                .with(tokio_console_layer())
176                .init();
177            None
178        }
179    }
180}
181
182/// Initialize logging with dual output for legacy headless call sites.
183///
184/// Headless mode logs to line-buffered stdout only.
185pub fn init_dual_logging(file_level: &str) {
186    let _ = init_logging(UiMode::Headless, file_level, false, false);
187}
188
189/// Initialize logging for the unified binary.
190///
191/// Headless mode logs to line-buffered stdout only.
192/// TUI mode logs to the in-memory TUI buffer and may also mirror to `debug.log`
193/// when explicitly enabled for local debugging sessions.
194#[must_use]
195pub fn init_logging(
196    ui_mode: UiMode,
197    file_level: &str,
198    capture_in_memory_logs: bool,
199    write_debug_log: bool,
200) -> Option<crate::tui::LogBuffer> {
201    match ui_mode {
202        UiMode::Headless => init_headless_subscriber(capture_in_memory_logs),
203        UiMode::Tui => init_tui_subscriber(file_level, capture_in_memory_logs, write_debug_log),
204    }
205}
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    #[test]
212    fn should_write_debug_log_requires_local_tui_and_debug_level() {
213        assert!(should_write_debug_log(UiMode::Tui, false, "debug"));
214        assert!(should_write_debug_log(UiMode::Tui, false, "trace"));
215        assert!(should_write_debug_log(
216            UiMode::Tui,
217            false,
218            "info,mycrate=debug"
219        ));
220        assert!(!should_write_debug_log(UiMode::Headless, false, "debug"));
221        assert!(!should_write_debug_log(UiMode::Tui, true, "debug"));
222        assert!(!should_write_debug_log(UiMode::Tui, false, "info"));
223        assert!(!should_write_debug_log(
224            UiMode::Tui,
225            false,
226            "warn,mycrate=info"
227        ));
228    }
229
230    #[test]
231    fn should_capture_in_memory_logs_skips_attached_clients() {
232        assert!(should_capture_in_memory_logs(UiMode::Tui, false, false));
233        assert!(!should_capture_in_memory_logs(UiMode::Tui, true, false));
234        assert!(should_capture_in_memory_logs(UiMode::Headless, false, true));
235        assert!(!should_capture_in_memory_logs(
236            UiMode::Headless,
237            false,
238            false
239        ));
240    }
241}