1use std::{env, fmt, io, sync::OnceLock, time::SystemTime};
5
6use anyhow::{Context, Result, anyhow};
7use clap::crate_name;
8use tracing::{
9 Event, Level, Subscriber,
10 field::{Field, Visit},
11};
12use tracing_appender::rolling::{self, Builder};
13use tracing_subscriber::{
14 EnvFilter, Layer,
15 fmt::{
16 FmtContext, FormatEvent, FormatFields, FormattedFields, format::Writer,
17 },
18 prelude::*,
19 registry::LookupSpan,
20};
21
22use crate::storage::{get_storage_dir, pc_settings::get_bool_setting};
23use crate::{cli::Invocation, utilities::COLUMN_UUID_DELIM};
24
25pub mod test_internal;
26pub mod test_subscriber;
27
28#[derive(Clone)]
31struct LogEventFormatter {
32 pid: u64,
33 sub_index: Option<String>,
34 service_name: Option<String>,
35}
36
37impl LogEventFormatter {
38 fn format_timestamp() -> String {
39 humantime::format_rfc3339_micros(SystemTime::now()).to_string()
40 }
41}
42
43struct MyVisitor {
44 file: Option<String>,
45 line: Option<u32>,
46}
47
48impl Visit for MyVisitor {
49 fn record_str(&mut self, field: &Field, value: &str) {
50 if field.name() == "log.file" {
51 self.file = Some(value.to_string());
52 }
53 }
54 fn record_u64(&mut self, field: &Field, value: u64) {
55 if field.name() == "log.line" {
56 self.line = u32::try_from(value).ok();
57 }
58 }
59 fn record_debug(&mut self, _field: &Field, _value: &dyn fmt::Debug) {
60 }
62}
63
64fn extract_file_line(event: &Event) -> (Option<String>, Option<u32>) {
65 let mut visitor = MyVisitor {
66 file: None,
67 line: None,
68 };
69 event.record(&mut visitor);
70 (visitor.file, visitor.line)
71}
72
73impl<S, N> FormatEvent<S, N> for LogEventFormatter
74where
75 S: Subscriber + for<'a> LookupSpan<'a>,
76 N: for<'a> FormatFields<'a> + 'static,
77{
78 fn format_event(
79 &self,
80 ctx: &FmtContext<'_, S, N>,
81 mut writer: Writer<'_>,
82 event: &Event<'_>,
83 ) -> fmt::Result {
84 let meta = event.metadata();
85
86 let target = meta
88 .module_path()
89 .or_else(|| Some(meta.target()))
90 .unwrap_or("unknown");
91
92 let (file, line) = extract_file_line(event);
93
94 let mut file = file
96 .or_else(|| meta.file().map(std::string::ToString::to_string))
97 .unwrap_or_else(|| "unknown".to_string());
98 if file.starts_with("src/") {
99 file = file.rsplit('/').next().unwrap_or(&file).to_string();
100 } else if meta.level() == &Level::DEBUG {
101 return Ok(());
102 }
103 let line = line.or_else(|| meta.line()).unwrap_or(0);
104
105 let mut column = "";
106 let mut buf = String::new();
108 let column_writer = Writer::new(&mut buf);
109 if ctx
110 .field_format()
111 .format_fields(column_writer, event)
112 .is_ok()
113 {
114 if let Some(idx) = buf.find(COLUMN_UUID_DELIM) {
115 let after = &buf[idx + COLUMN_UUID_DELIM.len()..];
116 let col_text = after.split_whitespace().next().unwrap_or("");
117 column = col_text;
118 }
119 }
120 let column_formatted;
121 if !column.is_empty() {
122 column_formatted = format!(":{column}");
123 column = &column_formatted;
124 }
125
126 let pid = self.pid;
127 let sub_idx = self.sub_index.as_deref().unwrap_or("");
128 let svc_name = self.service_name.as_deref().unwrap_or("user");
129 let ts = Self::format_timestamp();
130
131 write!(
132 &mut writer,
133 "[{ts} {} {file}:{line}{column} {target} {sub_idx}:{pid}/{svc_name}] ",
134 meta.level()
135 )?;
136
137 if let Some(scope) = ctx.event_scope() {
139 for span in scope.from_root() {
140 write!(writer, "{}", span.name())?;
141 if let Some(fields) = span
142 .extensions()
143 .get::<FormattedFields<N>>()
144 .filter(|f| !f.is_empty())
145 {
146 write!(writer, "{{{fields}}}")?;
147 }
148 write!(writer, ": ")?;
149 }
150 }
151
152 let mut event_buf = String::new();
154 {
155 let mut tmp_writer = Writer::new(&mut event_buf);
156 ctx.field_format()
157 .format_fields(tmp_writer.by_ref(), event)?;
158 }
159 if let Some(idx) = event_buf.find(COLUMN_UUID_DELIM) {
161 let before = &event_buf[..idx];
163 let after = &event_buf[idx + COLUMN_UUID_DELIM.len()..];
164 let after_trimmed = after
166 .split_whitespace()
167 .skip(1)
168 .collect::<Vec<_>>()
169 .join(" ");
170 write!(writer, "{before}{after_trimmed}")?;
171 } else {
172 write!(writer, "{event_buf}")?;
173 }
174 writeln!(writer)
175 }
176}
177
178fn build_default_filter_directives() -> String {
180 let crate_name = crate_name!();
181
182 if cfg!(debug_assertions) {
183 format!(
185 "warn,{crate_name}=debug,tower_http=debug,hyper=warn,axum::rejection=trace"
186 )
187 } else {
188 format!(
190 "warn,{crate_name}=warn,tower_http=debug,hyper=error,axum::rejection=warn"
191 )
192 }
193}
194
195fn create_env_filter() -> (EnvFilter, String) {
199 let default_directives = build_default_filter_directives();
200
201 let directives = match env::var("RUST_LOG") {
203 Ok(val) if !val.trim().is_empty() => val,
204 _ => default_directives.clone(),
205 };
206
207 let filter = EnvFilter::builder()
209 .with_default_directive(Level::INFO.into())
210 .parse(&directives)
211 .unwrap_or_else(|_| {
212 EnvFilter::builder()
213 .with_default_directive(Level::INFO.into())
214 .parse_lossy(&default_directives)
215 });
216
217 (filter, directives)
218}
219
220#[tracing::instrument]
221pub fn setup_logger(invocation: &Invocation) -> Result<()> {
222 let pid = u64::from(std::process::id());
223
224 let (sub_index, service_name) = match invocation {
225 Invocation::User(_) => {
226 (Some("workspace".to_string()), Some("workspace".to_string()))
227 }
228 Invocation::Subprocess(sub) => (
229 Some(sub.subprocess_index.to_string()),
230 Some(sub.service_name.clone()),
231 ),
232 _ => (None, None),
233 };
234
235 let (_, directives) = create_env_filter();
248
249 let stderr_formatter = LogEventFormatter {
251 pid,
252 sub_index: sub_index.clone(),
253 service_name: service_name.clone(),
254 };
255
256 let mut layers = Vec::new();
264
265 if cfg!(debug_assertions) || get_bool_setting("log_stack_file") {
267 layers.push(get_file_writer_layer(
268 invocation,
269 stderr_formatter.clone(),
270 &directives,
271 )?);
272 }
273 if cfg!(test) {
274 layers.push(get_mock_writer_layer(stderr_formatter.clone()));
275 }
276 let stderr_layer = get_stderr_layer(stderr_formatter, &directives)?;
277 layers.push(stderr_layer);
278
279 let result = tracing_subscriber::registry().with(layers).try_init();
280 match result {
281 Ok(()) => Ok(()),
282 Err(err) => Err(anyhow!(
283 "Failed to initialize logging in release mode: {err}"
284 )),
285 }
286}
287
288fn get_stderr_layer<S>(
289 formatter: LogEventFormatter,
290 directives: &str,
291) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>>
292where
293 S: Subscriber + for<'a> LookupSpan<'a>,
294{
295 let env_filter = EnvFilter::builder()
296 .with_default_directive(Level::INFO.into())
297 .parse(directives)
298 .with_context(|| format!("Failed to parse filter directives: {directives}"))?;
299
300 Ok(tracing_subscriber::fmt::layer()
301 .with_writer(io::stderr)
302 .with_test_writer()
304 .with_ansi(false)
305 .event_format(formatter)
306 .with_filter(env_filter)
307 .boxed())
308}
309
310fn get_mock_writer_layer<S>(
311 formatter: LogEventFormatter,
312) -> Box<dyn Layer<S> + Send + Sync + 'static>
313where
314 S: Subscriber + for<'a> LookupSpan<'a>,
315{
316 let filter_directives = if cfg!(feature = "tracing-test-no-env-filter") {
317 "trace".to_string()
318 } else {
319 let crate_name = module_path!()
320 .split(':')
321 .next()
322 .unwrap_or("unknown")
323 .to_string();
324 format!("{crate_name}=trace")
325 };
326 let env_filter = EnvFilter::builder()
327 .with_default_directive(Level::TRACE.into())
328 .parse_lossy(&filter_directives);
329
330 let mock_writer = crate::utilities::logging::test_internal::MockWriter::new(
331 crate::utilities::logging::test_internal::global_buf(),
332 );
333
334 tracing_subscriber::fmt::layer()
335 .with_writer(mock_writer)
336 .with_level(true)
337 .with_ansi(false)
338 .event_format(formatter)
339 .with_filter(env_filter)
340 .boxed()
341}
342
343fn get_file_writer_layer<S>(
344 invocation: &Invocation,
345 formatter: LogEventFormatter,
346 directives: &str,
347) -> Result<Box<dyn Layer<S> + Send + Sync + 'static>>
348where
349 S: Subscriber + for<'a> LookupSpan<'a>,
350{
351 let log_dir = get_storage_dir()
352 .context("failed to resolve storage dir for logs")?
353 .join("logs");
354 std::fs::create_dir_all(&log_dir)
355 .with_context(|| format!("creating log dir: {}", log_dir.display()))?;
356
357 let filename_base = match invocation {
359 Invocation::User(_) => "workspace".to_string(),
360 Invocation::Subprocess(sub) => sub.service_name.clone(),
361 _ => "app".to_string(),
362 };
363 let filename_base = if cfg!(test) {
365 format!("test-{filename_base}")
366 } else {
367 filename_base
368 };
369
370 let file_appender = Builder::new()
371 .rotation(rolling::Rotation::DAILY)
372 .max_log_files(7)
373 .filename_prefix(filename_base.to_string())
374 .filename_suffix("log")
375 .build(&log_dir)?;
376 let (nb_writer, guard) = tracing_appender::non_blocking(file_appender);
377
378 static APPENDER_GUARDS: OnceLock<
380 std::sync::Mutex<Vec<tracing_appender::non_blocking::WorkerGuard>>,
381 > = OnceLock::new();
382 let guards =
383 APPENDER_GUARDS.get_or_init(|| std::sync::Mutex::new(Vec::new()));
384 if let Ok(mut guards) = guards.lock() {
385 guards.push(guard);
386 }
387
388 let env_filter = EnvFilter::builder()
389 .with_default_directive(Level::INFO.into())
390 .parse(directives)
391 .with_context(|| format!("Failed to parse filter directives: {directives}"))?;
392
393 let file_layer = tracing_subscriber::fmt::layer()
394 .with_writer(nb_writer)
395 .event_format(formatter)
396 .with_ansi(false)
397 .with_filter(env_filter)
398 .boxed();
399
400 Ok(file_layer)
401}
402
403