1use std::{
2 fs::{self, OpenOptions},
3 io::{self, Write},
4 path::{Path, PathBuf},
5 sync::{Arc, LazyLock, Mutex, OnceLock},
6 time::Duration,
7};
8
9use chrono::{Local, SecondsFormat};
10use eyre::Context;
11use gammaloop_tracing_filter::{
12 strip_ansi_escape_codes, GammaDisplayFormat, GammaLogFilter, LogFormat, LogSink, LogStyle,
13 RoutedFieldsVisitor,
14};
15use indicatif::ProgressState;
16use tracing::{level_filters::LevelFilter, Event, Subscriber};
17use tracing_indicatif::{filter::IndicatifFilter, style::ProgressStyle, IndicatifLayer};
18use tracing_subscriber::field::RecordFields;
19use tracing_subscriber::Layer;
20use tracing_subscriber::{
21 filter::{filter_fn, FilterExt, Filtered},
22 fmt::FormattedFields,
23 fmt::{
24 self,
25 format::Writer,
26 writer::{BoxMakeWriter, MakeWriter},
27 FmtContext, FormatEvent, FormatFields,
28 },
29 layer::SubscriberExt,
30 registry::LookupSpan,
31 util::SubscriberInitExt,
32};
33
34use color_eyre::Result;
35
36fn file_filter_from(user_spec: &str) -> Result<GammaLogFilter> {
37 GammaLogFilter::parse_with_default(user_spec, Some(LevelFilter::WARN)).map_err(Into::into)
38}
39
40fn display_filter_from(user_spec: &str) -> Result<GammaLogFilter> {
41 GammaLogFilter::parse_with_default(user_spec, Some(LevelFilter::OFF))
42 .context(format!("Trying to parse filter spec: {user_spec}"))
43}
44
45const ENV_FILE_LOG_FILTER: &str = "GL_LOGFILE_FILTER";
46const ENV_NO_GL_HARD_WARNINGS: &str = "GL_NO_HARD_WARNINGS";
47const ENV_DISPLAY_LOG_FILTER: &str = "GL_DISPLAY_FILTER";
48const ENV_ALL_LOG_FILTER: &str = "GL_ALL_LOG_FILTER";
49
50#[derive(Debug, Clone)]
51struct StderrLogSpecState {
52 base_spec: String,
53 override_spec: Option<String>,
54}
55
56impl Default for StderrLogSpecState {
57 fn default() -> Self {
58 let base_spec = if let Ok(all) = std::env::var(ENV_ALL_LOG_FILTER) {
59 all
60 } else {
61 std::env::var(ENV_DISPLAY_LOG_FILTER)
62 .ok()
63 .unwrap_or_default()
64 };
65 Self {
66 base_spec,
67 override_spec: None,
68 }
69 }
70}
71
72impl StderrLogSpecState {
73 fn effective_spec(&self) -> &str {
74 self.override_spec
75 .as_deref()
76 .unwrap_or(self.base_spec.as_str())
77 }
78
79 fn effective_spec_string(&self) -> String {
80 self.effective_spec().to_string()
81 }
82}
83
84#[derive(Debug, Clone, Default)]
85struct FileLogSpecState {
86 base_spec: String,
87 override_spec: Option<String>,
88 hard_disabled_reason: Option<String>,
89}
90
91impl FileLogSpecState {
92 fn effective_spec(&self) -> &str {
93 self.override_spec
94 .as_deref()
95 .unwrap_or(self.base_spec.as_str())
96 }
97
98 fn effective_spec_string(&self) -> String {
99 self.effective_spec().to_string()
100 }
101
102 fn hard_disabled(&self) -> bool {
103 self.hard_disabled_reason.is_some()
104 }
105}
106
107static FILE_LOG_SPEC: LazyLock<Mutex<FileLogSpecState>> = LazyLock::new(|| {
109 let base_spec = if let Ok(all) = std::env::var(ENV_ALL_LOG_FILTER) {
110 all
111 } else {
112 std::env::var(ENV_FILE_LOG_FILTER).ok().unwrap_or_default()
113 };
114 Mutex::new(FileLogSpecState {
115 base_spec,
116 ..FileLogSpecState::default()
117 })
118});
119static STDERR_LOG_SPEC: LazyLock<Mutex<StderrLogSpecState>> =
120 LazyLock::new(|| Mutex::new(StderrLogSpecState::default()));
121
122type ReloadFilterFn = Box<dyn Fn(GammaLogFilter) -> Result<()> + Send + Sync>;
123
124struct FilterHandles {
125 file_reload: Option<ReloadFilterFn>,
126 stderr_reload: ReloadFilterFn,
127}
128
129static FILTER_HANDLES: OnceLock<FilterHandles> = OnceLock::new();
130
131fn detached_filter_handles(file_reload_enabled: bool) -> FilterHandles {
132 FilterHandles {
133 file_reload: file_reload_enabled.then(|| Box::new(|_| Ok(())) as ReloadFilterFn),
134 stderr_reload: Box::new(|_| Ok(())),
135 }
136}
137
138fn log_filter_env_override(specific_env: &str) -> Option<String> {
139 if let Ok(all) = std::env::var(ENV_ALL_LOG_FILTER) {
140 Some(all)
141 } else {
142 std::env::var(specific_env).ok()
143 }
144}
145
146struct LazyJsonWriterState {
147 dir: PathBuf,
148 filename: String,
149 file: Mutex<Option<fs::File>>,
150}
151
152#[derive(Clone)]
153struct LazyJsonMakeWriter {
154 state: Arc<LazyJsonWriterState>,
155}
156
157struct LazyJsonWriter {
158 state: Arc<LazyJsonWriterState>,
159 buffer: Vec<u8>,
160}
161
162impl LazyJsonMakeWriter {
163 fn new(dir: PathBuf, filename: String) -> Self {
164 Self {
165 state: Arc::new(LazyJsonWriterState {
166 dir,
167 filename,
168 file: Mutex::new(None),
169 }),
170 }
171 }
172}
173
174impl<'a> MakeWriter<'a> for LazyJsonMakeWriter {
175 type Writer = LazyJsonWriter;
176
177 fn make_writer(&'a self) -> Self::Writer {
178 LazyJsonWriter {
179 state: Arc::clone(&self.state),
180 buffer: Vec::new(),
181 }
182 }
183}
184
185impl io::Write for LazyJsonWriter {
186 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
187 self.buffer.extend_from_slice(buf);
188
189 while let Some(newline_index) = self.buffer.iter().position(|byte| *byte == b'\n') {
190 let line = self.buffer.drain(..=newline_index).collect::<Vec<_>>();
191 self.write_sanitized_line(&line)?;
192 }
193 Ok(buf.len())
194 }
195
196 fn flush(&mut self) -> io::Result<()> {
197 if !self.buffer.is_empty() {
198 let line = std::mem::take(&mut self.buffer);
199 self.write_sanitized_line(&line)?;
200 }
201 if let Some(file) = self.state.file.lock().unwrap().as_mut() {
202 file.flush()?;
203 }
204 Ok(())
205 }
206}
207
208impl LazyJsonWriter {
209 fn write_sanitized_line(&self, line: &[u8]) -> io::Result<()> {
210 let mut file = self.state.file.lock().unwrap();
211 if file.is_none() {
212 fs::create_dir_all(&self.state.dir)?;
213 let opened = OpenOptions::new()
214 .create(true)
215 .append(true)
216 .open(self.state.dir.join(&self.state.filename))?;
217 *file = Some(opened);
218 }
219
220 let file = file.as_mut().unwrap();
221 let line = sanitize_json_log_line(line);
222 file.write_all(&line)
223 }
224}
225
226fn sanitize_json_log_line(line: &[u8]) -> Vec<u8> {
227 let mut trailing_newline = false;
228 let line = if let Some(stripped) = line.strip_suffix(b"\n") {
229 trailing_newline = true;
230 stripped
231 } else {
232 line
233 };
234
235 let sanitized = match serde_json::from_slice::<serde_json::Value>(line) {
236 Ok(mut value) => {
237 strip_ansi_from_json_value(&mut value);
238 serde_json::to_vec(&value).unwrap_or_else(|_| line.to_vec())
239 }
240 Err(_) => line.to_vec(),
241 };
242
243 if trailing_newline {
244 let mut with_newline = sanitized;
245 with_newline.push(b'\n');
246 with_newline
247 } else {
248 sanitized
249 }
250}
251
252fn strip_ansi_from_json_value(value: &mut serde_json::Value) {
253 match value {
254 serde_json::Value::String(string) => {
255 *string = strip_ansi_escape_codes(string);
256 }
257 serde_json::Value::Array(items) => {
258 for item in items {
259 strip_ansi_from_json_value(item);
260 }
261 }
262 serde_json::Value::Object(map) => {
263 for value in map.values_mut() {
264 strip_ansi_from_json_value(value);
265 }
266 }
267 serde_json::Value::Null | serde_json::Value::Bool(_) | serde_json::Value::Number(_) => {}
268 }
269}
270
271fn directive_is_effectively_off(spec: &str) -> bool {
272 GammaLogFilter::is_effectively_off(spec)
273}
274
275fn file_log_disabled_error(reason: &str) -> color_eyre::Report {
276 eyre::eyre!(
277 "Cannot enable logfile logging because this session was started with the logfile logger disabled ({reason})"
278 )
279}
280
281pub fn configure_file_log_boot_mode(disabled: bool, reason: Option<&str>) -> Result<()> {
282 let mut state = FILE_LOG_SPEC.lock().unwrap();
283 state.hard_disabled_reason = if disabled {
284 Some(reason.unwrap_or("CLI boot option").to_string())
285 } else {
286 None
287 };
288 if state.hard_disabled() && !directive_is_effectively_off(state.effective_spec()) {
289 return Err(file_log_disabled_error(
290 state
291 .hard_disabled_reason
292 .as_deref()
293 .unwrap_or("CLI boot option"),
294 ));
295 }
296 Ok(())
297}
298
299pub fn file_log_boot_disabled_reason() -> Option<String> {
300 FILE_LOG_SPEC.lock().unwrap().hard_disabled_reason.clone()
301}
302
303pub fn set_file_log_filter(user_spec: impl AsRef<str>) -> Result<()> {
305 let user_spec = if let Some(file) = log_filter_env_override(ENV_FILE_LOG_FILTER) {
306 if std::env::var(ENV_NO_GL_HARD_WARNINGS).is_err() {
307 eprintln!(
308 "WARNING, file log filter is set to {file}, will override settings {}",
309 user_spec.as_ref()
310 );
311 }
312 file
313 } else {
314 user_spec.as_ref().to_string()
315 };
316 let effective_spec = {
317 let mut state = FILE_LOG_SPEC.lock().unwrap();
318 let old_base = state.base_spec.clone();
319 state.base_spec = user_spec;
320 let effective = state.effective_spec_string();
321 if let Some(reason) = state.hard_disabled_reason.clone() {
322 if !directive_is_effectively_off(&effective) {
323 state.base_spec = old_base;
324 return Err(file_log_disabled_error(&reason));
325 }
326 }
327 effective
328 };
329
330 let Some(handles) = FILTER_HANDLES.get() else {
331 return Ok(());
332 };
333 let Some(reload) = handles.file_reload.as_ref() else {
334 return Ok(());
335 };
336 let full_spec = file_filter_from(&effective_spec)?;
337 reload(full_spec)?;
338 Ok(())
339}
340
341pub fn set_file_log_filter_override(user_spec: Option<String>) -> Result<()> {
342 let effective_spec = {
343 let mut state = FILE_LOG_SPEC.lock().unwrap();
344 let old_override = state.override_spec.clone();
345 state.override_spec = user_spec;
346 let effective = state.effective_spec_string();
347 if let Some(reason) = state.hard_disabled_reason.clone() {
348 if !directive_is_effectively_off(&effective) {
349 state.override_spec = old_override;
350 return Err(file_log_disabled_error(&reason));
351 }
352 }
353 effective
354 };
355
356 let Some(handles) = FILTER_HANDLES.get() else {
357 return Ok(());
358 };
359 let Some(reload) = handles.file_reload.as_ref() else {
360 return Ok(());
361 };
362 let full_spec = file_filter_from(&effective_spec)?;
363 reload(full_spec)?;
364 Ok(())
365}
366
367pub fn clear_file_log_filter_override_on_settings_change() -> Result<()> {
368 set_file_log_filter_override(None)
369}
370
371pub fn set_stderr_log_filter(user_spec: impl AsRef<str>) -> Result<()> {
373 let user_spec = if let Some(display) = log_filter_env_override(ENV_DISPLAY_LOG_FILTER) {
374 if std::env::var(ENV_NO_GL_HARD_WARNINGS).is_err() {
375 eprintln!(
376 "WARNING, display log filter is set to {display}, will override settings {}",
377 user_spec.as_ref()
378 );
379 }
380 display
381 } else {
382 user_spec.as_ref().to_string()
383 };
384
385 let effective_spec = {
386 let mut state = STDERR_LOG_SPEC.lock().unwrap();
387 state.base_spec = user_spec;
388 state.effective_spec_string()
389 };
390
391 let Some(handles) = FILTER_HANDLES.get() else {
392 return Ok(());
393 };
394 let full_spec = display_filter_from(&effective_spec)?;
395 (handles.stderr_reload)(full_spec)?;
397 Ok(())
398}
399
400pub fn set_stderr_log_filter_override(user_spec: Option<String>) -> Result<()> {
401 let effective_spec = {
402 let mut state = STDERR_LOG_SPEC.lock().unwrap();
403 state.override_spec = user_spec;
404 state.effective_spec_string()
405 };
406
407 let Some(handles) = FILTER_HANDLES.get() else {
408 return Ok(());
409 };
410 let full_spec = display_filter_from(&effective_spec)?;
411 (handles.stderr_reload)(full_spec)?;
412 Ok(())
413}
414
415pub fn get_file_log_filter() -> String {
417 FILE_LOG_SPEC.lock().unwrap().effective_spec_string()
418}
419
420pub fn get_stderr_log_filter() -> String {
422 STDERR_LOG_SPEC.lock().unwrap().effective_spec_string()
423}
424
425pub fn get_stderr_log_filter_label() -> String {
426 let spec = get_stderr_log_filter();
427 collapse_scoped_gamma_level(&spec).unwrap_or(spec)
428}
429
430fn collapse_scoped_gamma_level(spec: &str) -> Option<String> {
431 let (left, right) = spec.split_once(',')?;
432 let left = left.trim().strip_prefix("gammaloop_api=")?;
433 let right = right.trim().strip_prefix("gammalooprs=")?;
434 (left == right).then(|| left.to_string())
435}
436
437fn elapsed_subsec(state: &ProgressState, writer: &mut dyn std::fmt::Write) {
438 let seconds = state.elapsed().as_secs();
439 let sub_seconds = (state.elapsed().as_millis() % 1000) / 100;
440 let _ = writer.write_str(&format!("{}.{}s", seconds, sub_seconds));
441}
442
443pub(crate) fn init_tracing(dir: impl AsRef<Path>, log_file_name: Option<String>) {
444 symbolica::GLOBAL_SETTINGS
445 .initialize_tracing
446 .store(false, std::sync::atomic::Ordering::Relaxed);
447 FILTER_HANDLES.get_or_init(|| {
448 let file_state = FILE_LOG_SPEC.lock().unwrap().clone();
449 let file_spec = file_state.effective_spec_string();
450 let file_filter = file_filter_from(&file_spec).unwrap_or_else(|err| {
452 eprintln!(
453 "Invalid initial file log filter '{file_spec}': {err}. Falling back to warn."
454 );
455 file_filter_from("").expect("default file log filter must parse")
456 });
457 let stderr_spec = STDERR_LOG_SPEC.lock().unwrap().effective_spec_string();
462 let stderr_filter = display_filter_from(&stderr_spec).unwrap_or_else(|err| {
464 eprintln!(
465 "Invalid initial display log filter '{stderr_spec}': {err}. Falling back to off."
466 );
467 display_filter_from("").expect("default display log filter must parse")
468 });
469 let (file_filter_layer, file_handle) = file_filter.clone().reloadable();
475 let (stderr_filter_layer, stderr_handle) = stderr_filter.clone().reloadable();
476
477 let ts = Local::now()
479 .to_rfc3339_opts(SecondsFormat::Millis, true)
480 .replace([':', '+'], "-"); let filename = if let Some(log_name) = log_file_name {
483 format!("gammalog-{log_name}.jsonl")
484 } else {
485 format!("gammalog-{ts}.jsonl")
486 };
487 let file_writer = BoxMakeWriter::new(LazyJsonMakeWriter::new(
488 dir.as_ref().to_path_buf(),
489 filename,
490 ));
491
492 let json = fmt::layer()
493 .event_format(FileJsonFmt)
494 .with_writer(file_writer);
495
496 let indicatif_layer = IndicatifLayer::new()
497 .with_span_field_formatter(IndicatifPbMsgFields{})
498 .with_max_progress_bars(1024, None) .with_progress_style( ProgressStyle::with_template(
499 "{color_start}{span_child_prefix}{span_fields} -- {wide_msg} {elapsed_subsec}{color_end}",
500 )
501 .unwrap()
502 .with_key(
503 "elapsed_subsec",
504 elapsed_subsec,
505 )
506 .with_key(
507 "color_start",
508 |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
509 let elapsed = state.elapsed();
510
511 if elapsed > Duration::from_secs(60) {
512 let _ = write!(writer, "\x1b[{}m", 1 + 30);
514 } else if elapsed > Duration::from_secs(4) {
515 let _ = write!(writer, "\x1b[{}m", 3 + 30);
517 }
518 },
519 )
520 .with_key(
521 "color_end",
522 |state: &ProgressState, writer: &mut dyn std::fmt::Write| {
523 if state.elapsed() > Duration::from_secs(4) {
524 let _ = write!(writer, "\x1b[0m");
525 }
526 },
527 ),
528 )
529 .with_span_child_prefix_symbol("↳ ")
530 .with_span_child_prefix_indent(" ");
531
532 let status_layer = fmt::layer()
534 .with_target(false)
535 .event_format(GammaDisplayFormat::dynamic(|| {
536 LOG_STYLE_STATE.lock().unwrap().effective_style()
537 }))
538 .with_writer(indicatif_layer.get_stderr_writer());
539
540 let subscriber = tracing_subscriber::registry()
541 .with(Filtered::new(status_layer, stderr_filter_layer))
542 .with(indicatif_layer.with_filter(
545 filter_fn(|metadata| metadata.is_span()).and(IndicatifFilter::new(false)),
546 ));
547
548 let init_result = if file_state.hard_disabled() {
549 subscriber.try_init()
550 } else {
551 subscriber
552 .with(Filtered::new(json, file_filter_layer))
553 .try_init()
554 };
555 if init_result.is_err() {
556 return detached_filter_handles(!file_state.hard_disabled());
557 }
558
559 let file_reload = (!file_state.hard_disabled()).then_some(Box::new(move |filter| {
560 file_handle.reload(filter);
561 Ok(())
562 }) as ReloadFilterFn);
563 let stderr_reload = Box::new(move |filter| {
564 stderr_handle.reload(filter);
565 Ok(())
566 }) as ReloadFilterFn;
567
568 FilterHandles {
569 file_reload,
570 stderr_reload,
571 }
572 });
573}
574
575#[derive(Debug, Clone, Default)]
576struct LogStyleState {
577 base_style: LogStyle,
578 format_override: Option<LogFormat>,
579}
580
581impl LogStyleState {
582 fn effective_style(&self) -> LogStyle {
583 let mut style = self.base_style.clone();
584 if let Some(log_format) = self.format_override {
585 style.log_format = log_format;
586 }
587 style
588 }
589}
590
591static LOG_STYLE_STATE: LazyLock<Mutex<LogStyleState>> =
592 LazyLock::new(|| Mutex::new(LogStyleState::default()));
593
594pub fn set_log_style(style: LogStyle) {
595 LOG_STYLE_STATE.lock().unwrap().base_style = style;
596}
597
598pub fn set_log_format_override(log_format: Option<LogFormat>) {
599 LOG_STYLE_STATE.lock().unwrap().format_override = log_format;
600}
601
602#[derive(Clone, Default)]
603struct IndicatifPbMsgFields;
604
605impl<'writer> FormatFields<'writer> for IndicatifPbMsgFields {
606 fn format_fields<R: RecordFields>(
607 &self,
608 mut writer: Writer<'writer>,
609 fields: R,
610 ) -> std::fmt::Result {
611 struct Visitor {
612 msg: Option<String>,
613 }
614
615 impl tracing::field::Visit for Visitor {
616 fn record_debug(&mut self, f: &tracing::field::Field, v: &dyn std::fmt::Debug) {
617 if f.name() == "indicatif.pb_msg" {
618 self.msg = Some(format!("{v:?}"));
619 }
620 }
621
622 fn record_str(&mut self, f: &tracing::field::Field, v: &str) {
623 if f.name() == "indicatif.pb_msg" {
624 self.msg = Some(v.to_string());
625 }
626 }
627 }
628
629 let mut v = Visitor { msg: None };
630 fields.record(&mut v);
631 if let Some(msg) = v.msg {
632 write!(writer, "{msg}")?;
633 }
634 Ok(())
635 }
636}
637
638#[derive(Clone, Default)]
639struct FileJsonFmt;
640
641impl<S, N> FormatEvent<S, N> for FileJsonFmt
642where
643 S: Subscriber + for<'a> LookupSpan<'a>,
644 N: for<'writer> FormatFields<'writer> + 'static,
645{
646 fn format_event(
647 &self,
648 ctx: &FmtContext<'_, S, N>,
649 mut w: Writer<'_>,
650 event: &Event<'_>,
651 ) -> std::fmt::Result {
652 let now = Local::now().to_rfc3339_opts(SecondsFormat::Millis, true);
653 let meta = event.metadata();
654
655 let mut visitor = RoutedFieldsVisitor::for_sink(LogSink::File);
656 event.record(&mut visitor);
657
658 let mut payload = serde_json::Map::new();
659 payload.insert("timestamp".to_string(), serde_json::Value::String(now));
660 payload.insert(
661 "level".to_string(),
662 serde_json::Value::String(meta.level().to_string()),
663 );
664 payload.insert(
665 "target".to_string(),
666 serde_json::Value::String(meta.target().to_string()),
667 );
668
669 if let Some(module_path) = meta.module_path() {
670 payload.insert(
671 "module_path".to_string(),
672 serde_json::Value::String(module_path.to_string()),
673 );
674 }
675 if let Some(file) = meta.file() {
676 payload.insert(
677 "file".to_string(),
678 serde_json::Value::String(file.to_string()),
679 );
680 }
681 if let Some(line) = meta.line() {
682 payload.insert("line".to_string(), line.into());
683 }
684 if let Some(message) = visitor.message {
685 payload.insert("message".to_string(), serde_json::Value::String(message));
686 }
687
688 for (key, value) in visitor.fields {
689 payload.insert(key, value);
690 }
691
692 if let Some(span) = ctx.lookup_current() {
693 let mut current_span = serde_json::Map::new();
694 current_span.insert(
695 "name".to_string(),
696 serde_json::Value::String(span.name().to_string()),
697 );
698 if let Some(fields) = span.extensions().get::<FormattedFields<N>>() {
699 let formatted = fields.to_string();
700 if !formatted.is_empty() {
701 current_span.insert("fields".to_string(), serde_json::Value::String(formatted));
702 }
703 }
704 payload.insert("span".to_string(), serde_json::Value::Object(current_span));
705 }
706
707 let line = serde_json::to_string(&payload).map_err(|_| std::fmt::Error)?;
708 w.write_str(&line)?;
709 w.write_char('\n')
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use tracing_subscriber::filter::LevelFilter;
716
717 use super::{
718 collapse_scoped_gamma_level, display_filter_from, file_filter_from, get_stderr_log_filter,
719 get_stderr_log_filter_label, set_stderr_log_filter, set_stderr_log_filter_override,
720 StderrLogSpecState, STDERR_LOG_SPEC,
721 };
722
723 struct StderrStateRestore(StderrLogSpecState);
724
725 impl Drop for StderrStateRestore {
726 fn drop(&mut self) {
727 *STDERR_LOG_SPEC.lock().unwrap() = self.0.clone();
728 }
729 }
730
731 fn preserve_stderr_filter_state() -> StderrStateRestore {
732 StderrStateRestore(STDERR_LOG_SPEC.lock().unwrap().clone())
733 }
734
735 #[test]
736 fn stderr_filter_override_supersedes_base_and_can_be_cleared() {
737 let _guard = crate::LOG_TEST_MUTEX
738 .lock()
739 .unwrap_or_else(|err| err.into_inner());
740 let _restore = preserve_stderr_filter_state();
741
742 set_stderr_log_filter("warn").unwrap();
743 set_stderr_log_filter_override(Some("debug".to_string())).unwrap();
744 assert_eq!(get_stderr_log_filter(), "debug");
745
746 set_stderr_log_filter("error").unwrap();
747 assert_eq!(get_stderr_log_filter(), "debug");
748
749 set_stderr_log_filter_override(None).unwrap();
750 assert_eq!(get_stderr_log_filter(), "error");
751 }
752
753 #[test]
754 fn scoped_gamma_directive_collapses_to_plain_level_label() {
755 assert_eq!(
756 collapse_scoped_gamma_level("gammaloop_api=debug,gammalooprs=debug"),
757 Some("debug".to_string())
758 );
759 assert_eq!(
760 collapse_scoped_gamma_level("gammaloop_api=debug,gammalooprs=info"),
761 None
762 );
763 assert_eq!(
764 collapse_scoped_gamma_level("symbolica=debug,gammalooprs=debug"),
765 None
766 );
767 }
768
769 #[test]
770 fn stderr_filter_label_prefers_collapsed_scoped_gamma_directive() {
771 let _guard = crate::LOG_TEST_MUTEX
772 .lock()
773 .unwrap_or_else(|err| err.into_inner());
774 let _restore = preserve_stderr_filter_state();
775
776 set_stderr_log_filter("warn").unwrap();
777 set_stderr_log_filter_override(Some("gammaloop_api=trace,gammalooprs=trace".to_string()))
778 .unwrap();
779 assert_eq!(get_stderr_log_filter_label(), "trace");
780
781 set_stderr_log_filter_override(None).unwrap();
782 assert_eq!(get_stderr_log_filter_label(), "warn");
783 }
784
785 #[test]
786 fn display_filter_empty_spec_defaults_to_off() {
787 assert_eq!(
788 display_filter_from("").unwrap().max_level_hint(),
789 Some(LevelFilter::OFF)
790 );
791 }
792
793 #[test]
794 fn file_filter_empty_spec_defaults_to_warn() {
795 assert_eq!(
796 file_filter_from("").unwrap().max_level_hint(),
797 Some(LevelFilter::WARN)
798 );
799 }
800
801 #[test]
802 fn display_filter_examples_with_multi_field_groups_parse() {
803 for spec in [
804 "gammalooprs::uv::forest[{generation,uv}]=debug",
805 "gammalooprs::uv::forest[{generation,uv,orientation,dump}]=debug",
806 "gammalooprs::uv::forest[{generation,uv,term}]=debug",
807 "gammalooprs::integrands::process::amplitude[{integration,subtraction}]=debug",
808 "gammalooprs::integrands::process::cross_section[{integration,sample,inspect}]=debug",
809 "gammalooprs::integrands::process::cross_section[{integration,cut,solver}]=debug",
810 ] {
811 assert!(
812 display_filter_from(spec).is_ok(),
813 "display filter example should parse: {spec}"
814 );
815 }
816 }
817
818 #[test]
819 fn file_filter_examples_with_multi_field_groups_parse() {
820 let spec = "gammalooprs::uv::forest[{generation,uv,orientation,dump}]=debug";
821 assert!(file_filter_from(spec).is_ok());
822 }
823}