1use 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 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
182pub fn init_dual_logging(file_level: &str) {
186 let _ = init_logging(UiMode::Headless, file_level, false, false);
187}
188
189#[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}