sos_logs/
logger.rs

1//! Log tracing output to disc.
2use crate::Result;
3use rev_buf_reader::RevBufReader;
4use sos_core::{Paths, UtcDateTime};
5use std::{
6    fs::File,
7    io::BufRead,
8    path::{Path, PathBuf},
9    sync::Arc,
10};
11use time::OffsetDateTime;
12use tracing_appender::rolling::{RollingFileAppender, Rotation};
13use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
14
15/// File name prefix for log files.
16#[doc(hidden)]
17pub const LOG_FILE_NAME: &str = "saveoursecrets.log";
18
19const DEFAULT_LOG_LEVEL: &str =
20    "sos=info,sos_net=debug,sos_bindings=debug,sos_backend=debug,sos_database=debug,sos_protocol=debug,sos_app=debug,sos_database_upgrader=debug";
21
22/// State of the log files on disc.
23pub struct LogFileStatus {
24    /// Path to the current log file.
25    pub current: PathBuf,
26    /// Size of the current log file.
27    pub current_size: u64,
28    /// List of all log files.
29    pub log_files: Vec<PathBuf>,
30    /// Total size of all log files.
31    pub total_size: u64,
32}
33
34/// Application logger.
35///
36/// When `debug_assertions` are enabled tracing output is written
37/// to stdout and to disc; for release builds tracing is just written
38/// to disc.
39pub struct Logger {
40    logs_dir: PathBuf,
41    name: String,
42}
43
44impl Default for Logger {
45    fn default() -> Self {
46        Self::new(None)
47    }
48}
49
50impl Logger {
51    /// Create a new logger using default paths.
52    ///
53    /// # Panics
54    ///
55    /// If the default data directory could not be determined.
56    pub fn new(name: Option<String>) -> Self {
57        Self::new_paths(Paths::new_client(Paths::data_dir().unwrap()), name)
58    }
59
60    /// Create a new logger with the given paths.
61    pub fn new_paths(paths: Arc<Paths>, name: Option<String>) -> Self {
62        let logs_dir = paths.logs_dir().to_owned();
63        Self {
64            logs_dir,
65            name: name.unwrap_or(LOG_FILE_NAME.to_string()),
66        }
67    }
68
69    /// Create a new logger with the given directory and file name.
70    pub fn new_dir(logs_dir: PathBuf, name: String) -> Self {
71        Self { logs_dir, name }
72    }
73
74    /// Initialize the tracing subscriber.
75    #[cfg(debug_assertions)]
76    pub fn init_subscriber(
77        &self,
78        default_log_level: Option<String>,
79    ) -> Result<()> {
80        let logs_dir = &self.logs_dir;
81
82        let logfile =
83            RollingFileAppender::new(Rotation::DAILY, logs_dir, &self.name);
84        let default_log_level =
85            default_log_level.unwrap_or_else(|| DEFAULT_LOG_LEVEL.to_owned());
86        let env_layer = tracing_subscriber::EnvFilter::new(
87            std::env::var("RUST_LOG").unwrap_or(default_log_level),
88        );
89
90        let file_layer = tracing_subscriber::fmt::layer()
91            .with_file(false)
92            .with_line_number(false)
93            .with_ansi(false)
94            .json()
95            .with_writer(logfile);
96
97        let fmt_layer = tracing_subscriber::fmt::layer()
98            .with_file(false)
99            .with_line_number(false)
100            .with_writer(std::io::stderr)
101            .with_target(false);
102
103        // NOTE: drop the error if already set so hot reload
104        // NOTE: does not panic in the GUI
105        let _ = tracing_subscriber::registry()
106            .with(env_layer)
107            .with(fmt_layer)
108            .with(file_layer)
109            .try_init();
110
111        Ok(())
112    }
113
114    /// Initialize a subscriber that writes to a file.
115    pub fn init_file_subscriber(
116        &self,
117        default_log_level: Option<String>,
118    ) -> Result<()> {
119        let logfile = RollingFileAppender::new(
120            Rotation::DAILY,
121            &self.logs_dir,
122            &self.name,
123        );
124        let default_log_level =
125            default_log_level.unwrap_or_else(|| DEFAULT_LOG_LEVEL.to_owned());
126        let env_layer = tracing_subscriber::EnvFilter::new(
127            std::env::var("RUST_LOG").unwrap_or(default_log_level),
128        );
129
130        let file_layer = tracing_subscriber::fmt::layer()
131            .with_file(false)
132            .with_line_number(false)
133            .with_ansi(false)
134            .json()
135            .with_writer(logfile);
136
137        // NOTE: drop the error if already set so hot reload
138        // NOTE: does not panic in the GUI
139        let _ = tracing_subscriber::registry()
140            .with(env_layer)
141            .with(file_layer)
142            .try_init();
143
144        Ok(())
145    }
146
147    /// Initialize the tracing subscriber.
148    #[cfg(not(debug_assertions))]
149    pub fn init_subscriber(
150        &self,
151        default_log_level: Option<String>,
152    ) -> Result<()> {
153        self.init_file_subscriber(default_log_level)
154    }
155
156    /// Log file status.
157    pub fn status(&self) -> Result<LogFileStatus> {
158        let current = self.current_log_file()?;
159        let current_size = std::fs::metadata(&current)?.len();
160        let log_files = self.log_file_paths()?;
161        let mut total_size = 0;
162        for path in &log_files {
163            total_size += std::fs::metadata(path)?.len();
164        }
165        Ok(LogFileStatus {
166            current,
167            current_size,
168            log_files,
169            total_size,
170        })
171    }
172
173    /// Load the tail of log records for the current file.
174    pub fn tail(&self, num_lines: Option<usize>) -> Result<Vec<String>> {
175        self.tail_file(num_lines, self.current_log_file()?)
176    }
177
178    /// Load the tail of log records for a file.
179    pub fn tail_file(
180        &self,
181        num_lines: Option<usize>,
182        path: impl AsRef<Path>,
183    ) -> Result<Vec<String>> {
184        let num_lines = num_lines.unwrap_or(100);
185        let file = File::open(path.as_ref())?;
186        let buf = RevBufReader::new(file);
187        let logs: Vec<String> =
188            buf.lines().take(num_lines).filter_map(|l| l.ok()).collect();
189        Ok(logs)
190    }
191
192    /// Delete all log files except the current log file.
193    pub fn delete_logs(&self) -> Result<()> {
194        let current = self.current_log_file()?;
195        let log_files = self.log_file_paths()?;
196        for path in log_files {
197            if path != current {
198                std::fs::remove_file(path)?;
199            }
200        }
201        // Workaround for set_len(0) failing with "Access Denied" on Windows
202        // SEE: https://github.com/rust-lang/rust/issues/105437
203        let _ = std::fs::OpenOptions::new()
204            .write(true)
205            .truncate(true)
206            .open(&current);
207        Ok(())
208    }
209
210    /// Get all the log files in the logs directory.
211    fn log_file_paths(&self) -> Result<Vec<PathBuf>> {
212        let mut files = Vec::new();
213        for entry in std::fs::read_dir(&self.logs_dir)? {
214            let entry = entry?;
215            let path = entry.path();
216            if let Some(name) = path.file_name() {
217                if name.to_string_lossy().starts_with(&self.name) {
218                    files.push(path);
219                }
220            }
221        }
222        Ok(files)
223    }
224
225    /// Log file for today.
226    fn current_log_file(&self) -> Result<PathBuf> {
227        let now: UtcDateTime = OffsetDateTime::now_utc().into();
228        let file = self.logs_dir.join(format!(
229            "{}.{}",
230            self.name,
231            now.format_simple_date()?
232        ));
233        Ok(file)
234    }
235}