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::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 println!(
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 println!(
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(IndicatifFilter::new(false)));
543
544 let init_result = if file_state.hard_disabled() {
545 subscriber.try_init()
546 } else {
547 subscriber
548 .with(Filtered::new(json, file_filter_layer))
549 .try_init()
550 };
551 if init_result.is_err() {
552 return detached_filter_handles(!file_state.hard_disabled());
553 }
554
555 let file_reload = (!file_state.hard_disabled()).then_some(Box::new(move |filter| {
556 file_handle.reload(filter);
557 Ok(())
558 }) as ReloadFilterFn);
559 let stderr_reload = Box::new(move |filter| {
560 stderr_handle.reload(filter);
561 Ok(())
562 }) as ReloadFilterFn;
563
564 FilterHandles {
565 file_reload,
566 stderr_reload,
567 }
568 });
569}
570
571#[derive(Debug, Clone, Default)]
572struct LogStyleState {
573 base_style: LogStyle,
574 format_override: Option<LogFormat>,
575}
576
577impl LogStyleState {
578 fn effective_style(&self) -> LogStyle {
579 let mut style = self.base_style.clone();
580 if let Some(log_format) = self.format_override {
581 style.log_format = log_format;
582 }
583 style
584 }
585}
586
587static LOG_STYLE_STATE: LazyLock<Mutex<LogStyleState>> =
588 LazyLock::new(|| Mutex::new(LogStyleState::default()));
589
590pub fn set_log_style(style: LogStyle) {
591 LOG_STYLE_STATE.lock().unwrap().base_style = style;
592}
593
594pub fn set_log_format_override(log_format: Option<LogFormat>) {
595 LOG_STYLE_STATE.lock().unwrap().format_override = log_format;
596}
597
598#[derive(Clone, Default)]
599struct IndicatifPbMsgFields;
600
601impl<'writer> FormatFields<'writer> for IndicatifPbMsgFields {
602 fn format_fields<R: RecordFields>(
603 &self,
604 mut writer: Writer<'writer>,
605 fields: R,
606 ) -> std::fmt::Result {
607 struct Visitor {
608 msg: Option<String>,
609 }
610
611 impl tracing::field::Visit for Visitor {
612 fn record_debug(&mut self, f: &tracing::field::Field, v: &dyn std::fmt::Debug) {
613 if f.name() == "indicatif.pb_msg" {
614 self.msg = Some(format!("{v:?}"));
615 }
616 }
617
618 fn record_str(&mut self, f: &tracing::field::Field, v: &str) {
619 if f.name() == "indicatif.pb_msg" {
620 self.msg = Some(v.to_string());
621 }
622 }
623 }
624
625 let mut v = Visitor { msg: None };
626 fields.record(&mut v);
627 if let Some(msg) = v.msg {
628 write!(writer, "{msg}")?;
629 }
630 Ok(())
631 }
632}
633
634#[derive(Clone, Default)]
635struct FileJsonFmt;
636
637impl<S, N> FormatEvent<S, N> for FileJsonFmt
638where
639 S: Subscriber + for<'a> LookupSpan<'a>,
640 N: for<'writer> FormatFields<'writer> + 'static,
641{
642 fn format_event(
643 &self,
644 ctx: &FmtContext<'_, S, N>,
645 mut w: Writer<'_>,
646 event: &Event<'_>,
647 ) -> std::fmt::Result {
648 let now = Local::now().to_rfc3339_opts(SecondsFormat::Millis, true);
649 let meta = event.metadata();
650
651 let mut visitor = RoutedFieldsVisitor::for_sink(LogSink::File);
652 event.record(&mut visitor);
653
654 let mut payload = serde_json::Map::new();
655 payload.insert("timestamp".to_string(), serde_json::Value::String(now));
656 payload.insert(
657 "level".to_string(),
658 serde_json::Value::String(meta.level().to_string()),
659 );
660 payload.insert(
661 "target".to_string(),
662 serde_json::Value::String(meta.target().to_string()),
663 );
664
665 if let Some(module_path) = meta.module_path() {
666 payload.insert(
667 "module_path".to_string(),
668 serde_json::Value::String(module_path.to_string()),
669 );
670 }
671 if let Some(file) = meta.file() {
672 payload.insert(
673 "file".to_string(),
674 serde_json::Value::String(file.to_string()),
675 );
676 }
677 if let Some(line) = meta.line() {
678 payload.insert("line".to_string(), line.into());
679 }
680 if let Some(message) = visitor.message {
681 payload.insert("message".to_string(), serde_json::Value::String(message));
682 }
683
684 for (key, value) in visitor.fields {
685 payload.insert(key, value);
686 }
687
688 if let Some(span) = ctx.lookup_current() {
689 let mut current_span = serde_json::Map::new();
690 current_span.insert(
691 "name".to_string(),
692 serde_json::Value::String(span.name().to_string()),
693 );
694 if let Some(fields) = span.extensions().get::<FormattedFields<N>>() {
695 let formatted = fields.to_string();
696 if !formatted.is_empty() {
697 current_span.insert("fields".to_string(), serde_json::Value::String(formatted));
698 }
699 }
700 payload.insert("span".to_string(), serde_json::Value::Object(current_span));
701 }
702
703 let line = serde_json::to_string(&payload).map_err(|_| std::fmt::Error)?;
704 w.write_str(&line)?;
705 w.write_char('\n')
706 }
707}
708
709#[cfg(test)]
710mod tests {
711 use tracing_subscriber::filter::LevelFilter;
712
713 use super::{
714 collapse_scoped_gamma_level, display_filter_from, file_filter_from, get_stderr_log_filter,
715 get_stderr_log_filter_label, set_stderr_log_filter, set_stderr_log_filter_override,
716 StderrLogSpecState, STDERR_LOG_SPEC,
717 };
718
719 struct StderrStateRestore(StderrLogSpecState);
720
721 impl Drop for StderrStateRestore {
722 fn drop(&mut self) {
723 *STDERR_LOG_SPEC.lock().unwrap() = self.0.clone();
724 }
725 }
726
727 fn preserve_stderr_filter_state() -> StderrStateRestore {
728 StderrStateRestore(STDERR_LOG_SPEC.lock().unwrap().clone())
729 }
730
731 #[test]
732 fn stderr_filter_override_supersedes_base_and_can_be_cleared() {
733 let _guard = crate::LOG_TEST_MUTEX
734 .lock()
735 .unwrap_or_else(|err| err.into_inner());
736 let _restore = preserve_stderr_filter_state();
737
738 set_stderr_log_filter("warn").unwrap();
739 set_stderr_log_filter_override(Some("debug".to_string())).unwrap();
740 assert_eq!(get_stderr_log_filter(), "debug");
741
742 set_stderr_log_filter("error").unwrap();
743 assert_eq!(get_stderr_log_filter(), "debug");
744
745 set_stderr_log_filter_override(None).unwrap();
746 assert_eq!(get_stderr_log_filter(), "error");
747 }
748
749 #[test]
750 fn scoped_gamma_directive_collapses_to_plain_level_label() {
751 assert_eq!(
752 collapse_scoped_gamma_level("gammaloop_api=debug,gammalooprs=debug"),
753 Some("debug".to_string())
754 );
755 assert_eq!(
756 collapse_scoped_gamma_level("gammaloop_api=debug,gammalooprs=info"),
757 None
758 );
759 assert_eq!(
760 collapse_scoped_gamma_level("symbolica=debug,gammalooprs=debug"),
761 None
762 );
763 }
764
765 #[test]
766 fn stderr_filter_label_prefers_collapsed_scoped_gamma_directive() {
767 let _guard = crate::LOG_TEST_MUTEX
768 .lock()
769 .unwrap_or_else(|err| err.into_inner());
770 let _restore = preserve_stderr_filter_state();
771
772 set_stderr_log_filter("warn").unwrap();
773 set_stderr_log_filter_override(Some("gammaloop_api=trace,gammalooprs=trace".to_string()))
774 .unwrap();
775 assert_eq!(get_stderr_log_filter_label(), "trace");
776
777 set_stderr_log_filter_override(None).unwrap();
778 assert_eq!(get_stderr_log_filter_label(), "warn");
779 }
780
781 #[test]
782 fn display_filter_empty_spec_defaults_to_off() {
783 assert_eq!(
784 display_filter_from("").unwrap().max_level_hint(),
785 Some(LevelFilter::OFF)
786 );
787 }
788
789 #[test]
790 fn file_filter_empty_spec_defaults_to_warn() {
791 assert_eq!(
792 file_filter_from("").unwrap().max_level_hint(),
793 Some(LevelFilter::WARN)
794 );
795 }
796
797 #[test]
798 fn display_filter_examples_with_multi_field_groups_parse() {
799 for spec in [
800 "gammalooprs::uv::forest[{generation,uv}]=debug",
801 "gammalooprs::uv::forest[{generation,uv,orientation,dump}]=debug",
802 "gammalooprs::uv::forest[{generation,uv,term}]=debug",
803 "gammalooprs::integrands::process::amplitude[{integration,subtraction}]=debug",
804 "gammalooprs::integrands::process::cross_section[{integration,sample,inspect}]=debug",
805 "gammalooprs::integrands::process::cross_section[{integration,cut,solver}]=debug",
806 ] {
807 assert!(
808 display_filter_from(spec).is_ok(),
809 "display filter example should parse: {spec}"
810 );
811 }
812 }
813
814 #[test]
815 fn file_filter_examples_with_multi_field_groups_parse() {
816 let spec = "gammalooprs::uv::forest[{generation,uv,orientation,dump}]=debug";
817 assert!(file_filter_from(spec).is_ok());
818 }
819}