rusty_express/support/
logger.rs

1#![allow(dead_code)]
2
3use std::env;
4use std::fmt;
5use std::fs;
6use std::fs::File;
7use std::io::Write;
8use std::net::SocketAddr;
9use std::path::PathBuf;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::thread;
12use std::time::Duration;
13
14use crate::channel::{self, SendError};
15use crate::chrono::{DateTime, Utc};
16use crate::parking_lot::{Mutex, Once, RwLock, ONCE_INIT};
17use crate::support::debug;
18
19lazy_static! {
20    static ref TEMP_STORE: Mutex<Vec<LogInfo>> = Mutex::new(Vec::new());
21    static ref CONFIG: RwLock<LoggerConfig> = RwLock::new(LoggerConfig::initialize(""));
22}
23
24const DEFAULT_LOCATION: &str = "./logs";
25
26static ONCE: Once = ONCE_INIT;
27static mut SENDER: Option<channel::Sender<LogInfo>> = None;
28static mut REFRESH_HANDLER: Option<thread::JoinHandle<()>> = None;
29static mut DUMPING_RUNNING: AtomicBool = AtomicBool::new(false);
30static mut LOG_WRITER: Option<Box<dyn LogWriter>> = None;
31
32#[derive(Debug)]
33pub enum InfoLevel {
34    Trace,
35    Debug,
36    Info,
37    Warn,
38    Error,
39    Critical,
40}
41
42impl fmt::Display for InfoLevel {
43    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
44        write!(f, "{:?}", self)
45    }
46}
47
48pub struct LogInfo {
49    message: String,
50    client: Option<SocketAddr>,
51    level: InfoLevel,
52    time: DateTime<Utc>,
53}
54
55impl fmt::Display for LogInfo {
56    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
57        match self.client {
58            Some(addr) => write!(
59                f,
60                "[{}] @ {} (from client {}): {}",
61                self.level,
62                self.time.to_string(),
63                addr.to_string(),
64                self.message
65            ),
66            None => write!(
67                f,
68                "[{}] @ {}: {}",
69                self.level,
70                self.time.to_string(),
71                self.message
72            ),
73        }
74    }
75}
76
77struct LoggerConfig {
78    id: String,
79    refresh_period: Duration,
80    log_folder_path: Option<PathBuf>,
81    meta_info_provider: Option<fn(bool) -> String>,
82    rx_handler: Option<thread::JoinHandle<()>>,
83}
84
85pub trait LogWriter {
86    fn dump(&self, log_store: Vec<LogInfo>) -> Result<(), Vec<LogInfo>>;
87}
88
89struct DefaultLogWriter {}
90
91impl DefaultLogWriter {
92    fn get_log_file(config: &LoggerConfig) -> Result<File, String> {
93        match config.log_folder_path {
94            Some(ref location) if location.is_dir() => create_dump_file(config.get_id(), location),
95            _ => create_dump_file(config.get_id(), &PathBuf::from(DEFAULT_LOCATION)),
96        }
97    }
98}
99
100impl LogWriter for DefaultLogWriter {
101    fn dump(&self, log_store: Vec<LogInfo>) -> Result<(), Vec<LogInfo>> {
102        let config = CONFIG.read();
103
104        if let Ok(mut file) = DefaultLogWriter::get_log_file(&config) {
105            // Now start a new dump
106            if let Some(meta_func) = config.meta_info_provider {
107                write_to_file(&mut file, &meta_func(true));
108            }
109
110            let mut content: String = String::new();
111
112            for (count, info) in log_store.iter().enumerate() {
113                content.push_str(&format_content(&info.level, &info.message, info.time));
114
115                if count % 10 == 0 {
116                    write_to_file(&mut file, &content);
117                    content.clear();
118                }
119            }
120
121            // write the remainder of the content
122            if !content.is_empty() {
123                write_to_file(&mut file, &content);
124            }
125
126            return Ok(());
127        }
128
129        Err(log_store)
130    }
131}
132
133impl LoggerConfig {
134    fn initialize(id: &str) -> Self {
135        LoggerConfig {
136            id: id.to_owned(),
137            refresh_period: Duration::from_secs(1800),
138            log_folder_path: None,
139            meta_info_provider: None,
140            rx_handler: None,
141        }
142    }
143
144    #[inline]
145    fn set_id(&mut self, id: &str) {
146        self.id = id.to_owned();
147    }
148
149    #[inline]
150    fn get_id(&self) -> String {
151        self.id.clone()
152    }
153
154    #[inline]
155    fn get_refresh_period(&self) -> Duration {
156        self.refresh_period.to_owned()
157    }
158
159    #[inline]
160    fn set_refresh_period(&mut self, period: Duration) {
161        self.refresh_period = period;
162    }
163
164    pub fn set_log_folder_path(&mut self, path: &str) {
165        let mut path_buff = PathBuf::new();
166
167        let location: Option<PathBuf> = if path.is_empty() {
168            match env::var_os("LOG_FOLDER_PATH") {
169                Some(p) => {
170                    path_buff.push(p);
171                    Some(path_buff)
172                }
173                None => None,
174            }
175        } else {
176            path_buff.push(path);
177            Some(path_buff)
178        };
179
180        if let Some(loc) = location {
181            if loc.as_path().is_dir() {
182                self.log_folder_path = Some(loc);
183            }
184        }
185    }
186
187    #[inline]
188    pub fn get_log_folder_path(&self) -> Option<PathBuf> {
189        self.log_folder_path.clone()
190    }
191}
192
193pub fn log(message: &str, level: InfoLevel, client: Option<SocketAddr>) -> Result<(), String> {
194    let info = LogInfo {
195        message: message.to_owned(),
196        client,
197        level,
198        time: Utc::now(),
199    };
200
201    unsafe {
202        if let Some(ref tx) = SENDER {
203            if let Err(SendError(msg)) = tx.send(info) {
204                return Err(format!("Failed to log the message: {}", msg.message));
205            }
206
207            return Ok(());
208        }
209    }
210
211    Err(String::from("The logging service is not running..."))
212}
213
214pub fn set_log_writer<T: LogWriter + 'static>(writer: T) {
215    unsafe {
216        LOG_WRITER = Some(Box::new(writer));
217    }
218}
219
220pub(crate) fn start(
221    period: Option<u64>,
222    log_folder_path: Option<&str>,
223    meta_info_provider: Option<fn(bool) -> String>,
224) {
225    {
226        let mut config = CONFIG.write();
227
228        if let Some(time) = period {
229            if time != 1800 {
230                (*config).refresh_period = Duration::from_secs(time);
231            }
232        }
233
234        if let Some(path) = log_folder_path {
235            (*config).set_log_folder_path(path);
236        }
237
238        (*config).meta_info_provider = meta_info_provider;
239    }
240
241    initialize();
242    set_log_writer(DefaultLogWriter {});
243}
244
245pub(crate) fn shutdown() {
246    stop_refresh();
247
248    let mut config = CONFIG.write();
249    if let Some(rx) = (*config).rx_handler.take() {
250        rx.join().unwrap_or_else(|err| {
251            eprintln!("Encountered error while closing the logger: {:?}", err);
252        });
253    }
254
255    let final_msg = LogInfo {
256        message: String::from("Shutting down the logging service..."),
257        client: None,
258        level: InfoLevel::Info,
259        time: Utc::now(),
260    };
261
262    unsafe {
263        if let Some(ref tx) = SENDER.take() {
264            if let Err(SendError(msg)) = tx.send(final_msg) {
265                debug::print("Failed to log the final message", debug::InfoLevel::Warning);
266            }
267        }
268    }
269
270    // Need to block because otherwise the lazy_static contents may go expired too soon
271    dump_log();
272}
273
274fn initialize() {
275    ONCE.call_once(|| {
276        let (tx, rx): (channel::Sender<LogInfo>, channel::Receiver<LogInfo>) = channel::bounded(16);
277
278        unsafe {
279            SENDER = Some(tx);
280        }
281
282        let mut config = CONFIG.write();
283
284        if let Some(ref path) = (*config).log_folder_path {
285            let refresh = (*config).refresh_period.as_secs();
286
287            println!(
288                "The logger has started, it will refresh log to folder {:?} every {} seconds",
289                path.to_str().unwrap(),
290                refresh
291            );
292
293            start_refresh((*config).refresh_period);
294        }
295
296        (*config).rx_handler = Some(thread::spawn(move || {
297            for info in rx {
298                let mut store = TEMP_STORE.lock();
299                (*store).push(info);
300            }
301        }));
302    });
303}
304
305fn start_refresh(period: Duration) {
306    unsafe {
307        if REFRESH_HANDLER.is_some() {
308            stop_refresh();
309        }
310
311        REFRESH_HANDLER = Some(thread::spawn(move || loop {
312            thread::sleep(period);
313            thread::spawn(|| {
314                dump_log();
315            });
316        }));
317    }
318}
319
320fn stop_refresh() {
321    unsafe {
322        if let Some(handler) = REFRESH_HANDLER.take() {
323            handler.join().unwrap_or_else(|err| {
324                eprintln!(
325                    "Failed to stop the log refresh service, error code: {:?}...",
326                    err
327                );
328            });
329        }
330    }
331}
332
333fn reset_refresh(period: Option<Duration>) {
334    thread::spawn(move || {
335        stop_refresh();
336
337        let new_period = {
338            let mut config = CONFIG.write();
339            if let Some(p) = period {
340                (*config).set_refresh_period(p);
341            }
342
343            (*config).get_refresh_period()
344        };
345
346        start_refresh(new_period);
347    });
348}
349
350fn dump_log() {
351    if unsafe { DUMPING_RUNNING.load(Ordering::SeqCst) } {
352        let config = CONFIG.read();
353
354        if let Ok(mut file) = DefaultLogWriter::get_log_file(&config) {
355            if let Some(meta_func) = config.meta_info_provider {
356                write_to_file(&mut file, &meta_func(false));
357            }
358
359            write_to_file(
360                &mut file,
361                &format_content(
362                    &InfoLevel::Info,
363                    "A dumping process is already in progress, skipping this scheduled dump.",
364                    Utc::now(),
365                ),
366            );
367        }
368
369        return;
370    }
371
372    let store: Vec<LogInfo> = {
373        let mut res = TEMP_STORE.lock();
374        (*res).drain(..).collect()
375    };
376
377    if !store.is_empty() {
378        unsafe {
379            if let Some(ref writer) = LOG_WRITER {
380                *DUMPING_RUNNING.get_mut() = true;
381
382                match writer.dump(store) {
383                    Ok(_) => {}
384                    Err(vec) => {
385                        let mut res = TEMP_STORE.lock();
386
387                        // can't write the result, put them back.
388                        (*res).extend(vec);
389                    }
390                }
391
392                *DUMPING_RUNNING.get_mut() = false;
393            }
394        }
395    }
396}
397
398fn write_to_file(file: &mut File, content: &str) {
399    file.write_all(content.as_bytes()).unwrap_or_else(|err| {
400        eprintln!("Failed to write to dump file: {}...", err);
401    });
402}
403
404fn format_content(level: &InfoLevel, message: &str, timestamp: DateTime<Utc>) -> String {
405    [
406        "\r\n[",
407        &level.to_string(),
408        "] @ ",
409        &timestamp.to_rfc3339(),
410        ": ",
411        message,
412    ]
413    .join("")
414}
415
416fn create_dump_file(id: String, loc: &PathBuf) -> Result<File, String> {
417    if !loc.as_path().is_dir() {
418        if let Err(e) = fs::create_dir_all(loc) {
419            return Err(format!("Failed to dump the logging information: {}", e));
420        }
421    }
422
423    let base = if id.is_empty() {
424        [&Utc::now().to_string(), ".txt"].join("")
425    } else {
426        [&id, "-", &Utc::now().to_string(), ".txt"].join("")
427    };
428
429    let mut path = loc.to_owned();
430    path.push(base);
431
432    match File::create(path.as_path()) {
433        Ok(file) => Ok(file),
434        _ => Err(String::from("Unable to create the dump file")),
435    }
436}