commonlibsse_ng\skse/
logger.rs

1use crate::rel::module::ModuleState;
2use crate::rex::win32::document_dir;
3use snafu::ResultExt as _;
4use std::path::{Path, PathBuf};
5#[cfg(feature = "tracing")]
6use std::sync::OnceLock;
7#[cfg(feature = "tracing")]
8use tracing_subscriber::{
9    Registry,
10    filter::LevelFilter,
11    fmt,
12    prelude::*,
13    reload::{self, Handle},
14};
15
16/// Get the log directory.(e.g. `$USER/documents/Skyrim Special Edition/SKSE`)
17///
18/// # Errors
19/// Returns an error if the document dir could not be obtained
20///
21/// # Note
22/// Searching in all diminutions where the current directory is `[..]/steamapps/common/Skyrim Special Edition`.
23pub fn log_directory() -> Result<PathBuf, LogInitError> {
24    let mut path = document_dir().map_err(|_| LogInitError::NotFoundDocumentDir)?;
25    path.push("My Games");
26
27    let runtime =
28        ModuleState::map_or_init(|module| module.runtime).context(UnexpectedModuleStateSnafu)?;
29
30    if runtime.is_vr() {
31        path.push("Skyrim VR");
32    } else if Path::new("steam_api64.dll").exists() {
33        if Path::new("openvr_api.dll").exists() {
34            path.push("Skyrim VR");
35
36        // ? The behavior of writing to `Skyrim.INI` seems to be incorrect.
37        // - See: https://www.nexusmods.com/skyrimspecialedition/mods/144932?tab=description
38        } else {
39            path.push("Skyrim Special Edition");
40        }
41    } else {
42        path.push("Skyrim Special Edition GOG");
43    }
44    path.push("SKSE");
45    Ok(path)
46}
47
48/// Global variable to allow dynamic level changes in logger.
49#[cfg(feature = "tracing")]
50static RELOAD_HANDLE: OnceLock<Handle<LevelFilter, Registry>> = OnceLock::new();
51#[cfg(feature = "tracing")]
52static GUARD: OnceLock<tracing_appender::non_blocking::WorkerGuard> = OnceLock::new();
53
54/// Initializes logger.
55///
56/// # Errors
57/// - If the directory for logs is not found
58/// - Logger did not have permission to create files
59#[cfg(feature = "tracing")]
60pub fn init<P, D>(log_dir: P, file_name: D, level: LevelFilter) -> Result<(), LogInitError>
61where
62    P: AsRef<Path>,
63    D: AsRef<Path>,
64{
65    _init(log_dir.as_ref(), file_name.as_ref(), level)
66}
67
68/// Initializes logger with skse log directory.
69///
70/// # Errors
71/// - If the directory for logs is not found
72/// - Logger did not have permission to create files
73#[cfg(feature = "tracing")]
74pub fn init_with_log_dir<P>(file_name: P, level: LevelFilter) -> Result<(), LogInitError>
75where
76    P: AsRef<Path>,
77{
78    _init(&log_directory()?, file_name.as_ref(), level)
79}
80
81#[cfg(feature = "tracing")]
82fn _init(log_dir: &Path, file_name: &Path, level: LevelFilter) -> Result<(), LogInitError> {
83    use tracing_appender::non_blocking::NonBlockingBuilder;
84
85    let _ = std::fs::create_dir_all(log_dir);
86    let file = std::fs::File::create(log_dir.join(file_name))
87        .map_err(|_e| LogInitError::FailedCreateLogFile)?;
88    let (non_blocking, guard) = NonBlockingBuilder::default().finish(file);
89
90    // Unable `pretty()` & `with_ansi(false)` combination in `#[tracing::instrument]`
91    // ref: https://github.com/tokio-rs/tracing/issues/1310
92    let fmt_layer = fmt::layer()
93        .compact()
94        .with_ansi(false)
95        .with_file(true)
96        .with_line_number(true)
97        .with_target(false)
98        .with_writer(non_blocking);
99
100    let (filter, reload_handle) = reload::Layer::new(level);
101    tracing_subscriber::registry().with(filter).with(fmt_layer).init();
102
103    GUARD.set(guard).map_err(|_e| LogInitError::FailedInitLog)?;
104    RELOAD_HANDLE.set(reload_handle).map_err(|_e| LogInitError::FailedInitLog)
105}
106
107/// If unknown log level, fallback to `error`.
108///
109/// # Errors
110/// If logger uninitialized.
111#[cfg(feature = "tracing")]
112pub fn change_level(log_level: &str) -> Result<(), LogReloadError> {
113    use snafu::ResultExt as _;
114
115    let new_filter =
116        <LevelFilter as core::str::FromStr>::from_str(log_level).unwrap_or_else(|_e| {
117            tracing::warn!("Unknown log level: {log_level}. Fallback to `error`");
118            LevelFilter::ERROR
119        });
120
121    RELOAD_HANDLE.get().map_or(Err(LogReloadError::UninitLog), |log| {
122        log.modify(|filter| *filter = new_filter).context(ReloadSnafu)
123    })
124}
125
126/// Error that may occur when changing logger settings
127#[cfg(feature = "tracing")]
128#[derive(Debug, snafu::Snafu)]
129pub enum LogReloadError {
130    /// Logger uninitialized.
131    UninitLog,
132
133    /// Failed to change the log level: {source}
134    Reload { source: tracing_subscriber::reload::Error },
135}
136
137/// Possible errors during logger initialization
138#[derive(Debug, snafu::Snafu)]
139pub enum LogInitError {
140    /// The logger could not be initialized because the document directory was not found.
141    NotFoundDocumentDir,
142
143    /// Logger could not be initialized because runtime information could not be obtained.: {source}
144    UnexpectedModuleState { source: crate::rel::module::ModuleStateError },
145
146    /// Failed to create a log file.
147    FailedCreateLogFile,
148
149    /// Failed to Initialize a log.
150    FailedInitLog,
151}
152
153#[cfg(feature = "test_on_ci")]
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_log_directory() {
160        let log_dir = log_directory().unwrap();
161        println!("{}", log_dir.display());
162    }
163}