epik/
logging.rs

1use std::fs::File;
2use std::path::PathBuf;
3
4use chrono::Utc;
5use simplelog::{CombinedLogger, ConfigBuilder, LevelFilter, SharedLogger, SimpleLogger, WriteLogger};
6
7use crate::config::Config;
8use crate::Result;
9
10fn parse_level(level: &str) -> LevelFilter {
11    match level.to_lowercase().as_str() {
12        "trace" => LevelFilter::Trace,
13        "debug" => LevelFilter::Debug,
14        "warn" => LevelFilter::Warn,
15        "error" => LevelFilter::Error,
16        _ => LevelFilter::Info,
17    }
18}
19
20pub fn init_logging(config: &Config, cli_verbose: bool) -> Result<()> {
21    let level = if cli_verbose {
22        LevelFilter::Debug
23    } else {
24        parse_level(&config.log_level)
25    };
26
27    let config_builder = ConfigBuilder::new()
28        .set_time_format_rfc3339()
29        .set_thread_level(LevelFilter::Off)
30        .set_location_level(LevelFilter::Error)
31        .build();
32
33    let mut loggers: Vec<Box<dyn SharedLogger>> = Vec::new();
34
35    // Terminal logger (stderr)
36    let term_logger: Box<dyn SharedLogger> = SimpleLogger::new(level, config_builder.clone());
37    loggers.push(term_logger);
38
39    // Optional file logger
40    if config.log_to_file {
41        let log_dir = Config::data_dir()?.join("logs");
42        std::fs::create_dir_all(&log_dir)?;
43        let log_file = log_dir.join("epik.log");
44        let file = File::create(&log_file)?;
45        loggers.push(WriteLogger::new(level, config_builder.clone(), file));
46    }
47
48    if let Err(e) = CombinedLogger::init(loggers) {
49        // If logger is already initialized (e.g., tests), don't fail startup
50        eprintln!("Logger already initialized: {}", e);
51    }
52
53    log::set_max_level(level);
54    Ok(())
55}
56
57pub fn install_panic_hook(enabled: bool, data_dir: PathBuf) {
58    if !enabled {
59        return;
60    }
61
62    let report_dir = data_dir.join("crash_reports");
63    let _ = std::fs::create_dir_all(&report_dir);
64
65    std::panic::set_hook(Box::new(move |info| {
66        let timestamp = Utc::now().format("%Y%m%d-%H%M%S");
67        let file_path = report_dir.join(format!("crash-{}.log", timestamp));
68        let mut report = String::new();
69
70        report.push_str("Epik Launcher Crash Report\n");
71        report.push_str(&format!("Timestamp: {}\n", Utc::now().to_rfc3339()));
72        report.push_str(&format!("Thread: {:?}\n", std::thread::current().name()));
73
74        if let Some(location) = info.location() {
75            report.push_str(&format!(
76                "Location: file {} line {} col {}\n",
77                location.file(),
78                location.line(),
79                location.column()
80            ));
81        }
82
83        if let Some(s) = info.payload().downcast_ref::<&str>() {
84            report.push_str(&format!("Payload: {}\n", s));
85        } else if let Some(s) = info.payload().downcast_ref::<String>() {
86            report.push_str(&format!("Payload: {}\n", s));
87        } else {
88            report.push_str("Payload: <unknown>\n");
89        }
90
91        if let Ok(mut file) = File::create(&file_path) {
92            use std::io::Write;
93            let _ = file.write_all(report.as_bytes());
94        }
95
96        eprintln!("A crash report was written to {:?}", file_path);
97    }));
98}