Skip to main content

solverforge_console/
init.rs

1use std::sync::OnceLock;
2
3use tracing_subscriber::layer::SubscriberExt;
4use tracing_subscriber::util::SubscriberInitExt;
5use tracing_subscriber::EnvFilter;
6
7use crate::banner;
8use crate::SolverConsoleLayer;
9
10static INIT: OnceLock<()> = OnceLock::new();
11
12/// Initializes the solver console output.
13///
14/// Safe to call multiple times - only the first call has effect.
15/// Prints the SolverForge banner and sets up tracing.
16pub fn init() {
17    INIT.get_or_init(|| {
18        banner::print_banner();
19
20        #[cfg(feature = "verbose-logging")]
21        let solver_level = "solverforge_solver=debug";
22        #[cfg(not(feature = "verbose-logging"))]
23        let solver_level = "solverforge_solver=info";
24
25        let rust_log = std::env::var("RUST_LOG").ok();
26        let rust_log = rust_log.as_deref();
27
28        let mut filter = EnvFilter::builder()
29            .with_default_directive(solver_level.parse().unwrap())
30            .from_env_lossy();
31
32        if !rust_log_has_directive_for(rust_log, "solverforge_solver")
33            && !rust_log_has_global_trace(rust_log)
34        {
35            filter = filter.add_directive(solver_level.parse().unwrap());
36        }
37
38        if !rust_log_has_directive_for(rust_log, "solverforge_dynamic")
39            && !rust_log_has_global_trace(rust_log)
40        {
41            filter = filter.add_directive("solverforge_dynamic=info".parse().unwrap());
42        }
43
44        let _ = tracing_subscriber::registry()
45            .with(filter)
46            .with(SolverConsoleLayer)
47            .try_init();
48    });
49}
50
51fn rust_log_has_directive_for(rust_log: Option<&str>, target: &str) -> bool {
52    rust_log
53        .into_iter()
54        .flat_map(|value| value.split(','))
55        .map(str::trim)
56        .filter(|directive| !directive.is_empty())
57        .filter_map(|directive| directive.split_once('=').map(|(target, _)| target.trim()))
58        .any(|directive_target| {
59            directive_target == target
60                || directive_target
61                    .strip_prefix(target)
62                    .is_some_and(|rest| rest.starts_with("::"))
63        })
64}
65
66fn rust_log_has_global_trace(rust_log: Option<&str>) -> bool {
67    rust_log
68        .into_iter()
69        .flat_map(|value| value.split(','))
70        .map(|directive| directive.trim().to_ascii_lowercase())
71        .any(|directive| directive == "trace")
72}
73
74#[cfg(test)]
75mod tests {
76    use super::{rust_log_has_directive_for, rust_log_has_global_trace};
77
78    #[test]
79    fn unrelated_rust_log_does_not_disable_solver_console_defaults() {
80        let rust_log = Some("warn,imap_codec=error,imap_client=error");
81
82        assert!(!rust_log_has_directive_for(rust_log, "solverforge_solver"));
83        assert!(!rust_log_has_directive_for(rust_log, "solverforge_dynamic"));
84        assert!(!rust_log_has_global_trace(rust_log));
85    }
86
87    #[test]
88    fn explicit_solver_target_disables_default_solver_directive() {
89        assert!(rust_log_has_directive_for(
90            Some("warn,solverforge_solver=trace"),
91            "solverforge_solver"
92        ));
93        assert!(rust_log_has_directive_for(
94            Some("solverforge_solver::phase=trace"),
95            "solverforge_solver"
96        ));
97    }
98
99    #[test]
100    fn global_trace_disables_default_solver_directives() {
101        assert!(rust_log_has_global_trace(Some(
102            "trace,imap_codec=error,imap_client=error"
103        )));
104        assert!(!rust_log_has_global_trace(Some(
105            "warn,solverforge_solver=trace"
106        )));
107    }
108}