1use std::borrow::Cow;
41use std::fs;
42use std::io::prelude::*;
43use std::path::PathBuf;
44use std::sync::{Arc, Mutex};
45
46use log::Log;
47use thiserror::Error;
48
49use powerpack_env as env;
50
51pub use log::LevelFilter;
52
53#[derive(Debug, Error)]
55pub enum Error {
56 #[error("home directory not found")]
58 NoHomeDir,
59
60 #[error("io error")]
62 Io(#[from] std::io::Error),
63
64 #[error("failed to set logger")]
66 SetLogger(#[from] log::SetLoggerError),
67}
68
69#[derive(Debug, Clone)]
71pub struct Logger {
72 file: Arc<Mutex<fs::File>>,
73 max_level: LevelFilter,
74}
75
76#[derive(Debug, Clone)]
78pub struct Builder {
79 directory: Option<PathBuf>,
80 filename: Option<Cow<'static, str>>,
81 max_level: Option<LevelFilter>,
82}
83
84impl Default for Builder {
85 #[inline]
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl Builder {
92 #[inline]
94 pub fn new() -> Self {
95 Self {
96 directory: None,
97 filename: None,
98 max_level: None,
99 }
100 }
101
102 #[inline]
106 pub fn directory(mut self, dir: impl Into<PathBuf>) -> Self {
107 self.directory = Some(dir.into());
108 self
109 }
110
111 #[inline]
115 pub fn filename(mut self, filename: impl Into<Cow<'static, str>>) -> Self {
116 self.filename = Some(filename.into());
117 self
118 }
119
120 #[inline]
122 pub fn max_level(mut self, max_level: LevelFilter) -> Self {
123 self.max_level = Some(max_level);
124 self
125 }
126
127 fn build(self) -> Result<Logger, Error> {
128 let directory = match self.directory {
129 Some(directory) => directory,
130 None => env::try_workflow_cache_or_default().ok_or(Error::NoHomeDir)?,
131 };
132 let filename = self.filename.as_deref().unwrap_or("powerpack.log");
133 let path = directory.join(filename);
134
135 fs::create_dir_all(directory)?;
136
137 let file = Arc::new(Mutex::new(
138 fs::OpenOptions::new()
139 .create(true)
140 .append(true)
141 .open(path)?,
142 ));
143
144 let max_level = self.max_level.unwrap_or_else(|| {
145 if env::is_debug() {
146 LevelFilter::Debug
147 } else {
148 LevelFilter::Info
149 }
150 });
151
152 Ok(Logger { file, max_level })
153 }
154
155 #[inline]
157 pub fn try_init(self) -> Result<(), Error> {
158 let logger = self.build()?;
159 let max_level = logger.max_level;
160 log::set_boxed_logger(Box::new(logger))?;
161 log::set_max_level(max_level);
162 Ok(())
163 }
164
165 #[track_caller]
174 #[inline]
175 pub fn init(self) {
176 self.try_init().expect("failed to initialize logger");
177 }
178
179 #[track_caller]
185 #[inline]
186 pub fn init_idempotent(self) {
187 match self.try_init() {
188 Ok(()) => {}
189 Err(Error::SetLogger(_)) => {}
190 r => r.expect("failed to initialize logger"),
191 }
192 }
193}
194
195impl Logger {
196 fn try_log(&self, record: &log::Record) -> Result<(), Box<dyn std::error::Error + '_>> {
197 let time = jiff::Timestamp::now().strftime("%Y-%m-%dT%H:%M:%S");
198 let mut f = self.file.lock()?;
199 writeln!(f, "[{}] [{}] {}", time, record.level(), record.args())?;
200 f.flush()?;
201 Ok(())
202 }
203
204 fn try_flush(&self) -> Result<(), Box<dyn std::error::Error + '_>> {
205 let mut f = self.file.lock()?;
206 f.flush()?;
207 Ok(())
208 }
209}
210
211impl Log for Logger {
212 fn enabled(&self, metadata: &log::Metadata) -> bool {
213 metadata.level() <= self.max_level
214 }
215
216 fn log(&self, record: &log::Record) {
217 if self.enabled(record.metadata()) {
218 self.try_log(record).expect("failed to log message");
219 }
220 }
221
222 fn flush(&self) {
223 self.try_flush().expect("failed to flush log file");
224 }
225}