arcs_logging_rs/
lib.rs

1mod r#macro; // Literal identifier syntax.
2pub mod r#trait;
3pub mod structs;
4pub mod webwriter;
5mod size_limit;
6mod signals;
7
8use lazy_init::Lazy;
9use r#macro::logging_parts;
10use lazy_static::lazy_static;
11
12use size_limit::CircularFileBuffer;
13use r#trait::WriteImmut;
14use structs::{PoisonErrorWrapper, ErrorWrapper};
15use url::Url;
16use webwriter::WebWriter;
17
18use std::collections::HashMap;
19use smallvec::{SmallVec, smallvec};
20
21use log::{LevelFilter, Metadata, Record};
22
23use std::fmt::{Display, Formatter};
24use std::sync::{RwLock, Arc};
25use chrono::{DateTime, Utc};
26
27
28use std::io::{stderr, stdout, ErrorKind, Write};
29use std::fs::OpenOptions;
30use std::path::Path;
31
32
33pub (crate) use std::io::{Error as IOError, Result as IOResult};
34
35#[doc(hidden)]
36pub mod __internal_redirects {
37    pub use log::{trace, debug, info, warn, error};
38}
39pub use log::Level;
40pub use arcs_logging_rs_proc_macro::with_target;
41use const_format::concatcp;
42
43macro_rules! color_text_fmt {
44    (bold $color_sequence:literal, $formatting_lit:literal, $text:expr) => {
45        format_args!(
46            "{}{}{}",
47            concatcp!("\x1b[1m", "\x1b[", $color_sequence, "m"),
48            format_args!($formatting_lit, $text),
49            "\x1b[0m",
50        )
51    };
52    ($color_sequence:literal, $formatting_lit:literal, $text:expr) => {
53        format_args!(
54            "{}{}{}",
55            concatcp!("\x1b[", $color_sequence, "m"),
56            format_args!($formatting_lit, $text),
57            "\x1b[0m",
58        )
59    };
60    (option bold $color_sequence:literal, $formatting_lit:literal, $text:expr) => {
61        {
62            let module_path = $text;
63            module_path.and(
64                Some(format_args!(
65                    "{}{}{}",
66                    concatcp!("\x1b[1m", "\x1b[", $color_sequence, "m"),
67                    format!($formatting_lit, $text.unwrap()),
68                    "\x1b[0m",
69                ))
70            )
71        }
72    };
73    (option $color_sequence:literal, $formatting_lit:literal, $text:expr) => {
74        {
75            let module_path = $text;
76            module_path.and(
77                Some(format_args!(
78                    "{}{}{}",
79                    concatcp!("\x1b[", $color_sequence, "m"),
80                    format!($formatting_lit, $text.unwrap()),
81                    "\x1b[0m",
82                ))
83            )
84        }
85    };
86}
87
88
89pub type LogLocationTargetMap<'a> = HashMap<Level, SmallVec<[LogLocationTarget<'a>; 6]>>;
90
91#[derive(Debug)]
92pub enum LogLocationTarget<'a> {
93    StdOut,
94    StdErr,
95    File(&'a Path, Option<u64>),
96    WebWriter(&'a str),
97}
98
99#[derive(Debug, Clone)]
100enum WritableLogLocationTarget {
101    StdOut,
102    StdErr,
103    File(Arc<RwLock<CircularFileBuffer>>),
104    WebWriter(Arc<WebWriter>),
105}
106
107impl WriteImmut for WritableLogLocationTarget {
108    fn write(&self, buf: &[u8]) -> IOResult<usize> {
109
110        match self {
111            WritableLogLocationTarget::StdOut => Write::write(&mut stdout(), buf),
112            WritableLogLocationTarget::StdErr => Write::write(&mut stderr(), buf),
113            WritableLogLocationTarget::File(f) => f.write().map_or_else(
114                |err| {
115                    Err(IOError::new(
116                        ErrorKind::Other,
117                        PoisonErrorWrapper::from(err),
118                    ))
119                },
120                |mut file| file.write(buf),
121            ),
122            WritableLogLocationTarget::WebWriter(w) => w.add_line(buf),
123        }
124    }
125    fn write_vectored(&self, bufs: &[std::io::IoSlice<'_>]) -> IOResult<usize> {
126        self.write(
127            bufs.iter()
128                .find(|buf| !buf.is_empty())
129                .map_or(&[][..], |buf| &**buf),
130        )
131    }
132    fn is_write_vectored(&self) -> bool {
133        false
134    }
135
136    fn flush(&self) -> IOResult<()> {
137        match self {
138            WritableLogLocationTarget::StdOut => stdout().flush(),
139            WritableLogLocationTarget::StdErr => stdout().flush(),
140            WritableLogLocationTarget::File(f) => f.write().map_or_else(
141                |err| {
142                    Err(IOError::new(
143                        ErrorKind::Other,
144                        PoisonErrorWrapper::from(err),
145                    ))
146                },
147                |mut file| {
148                    file.flush()
149                },
150            ),
151            WritableLogLocationTarget::WebWriter(w) => w.flush(),
152        }
153    }
154}
155
156impl Write for WritableLogLocationTarget {
157    fn write(&mut self, buf: &[u8]) -> IOResult<usize> {
158        WriteImmut::write(self, buf)
159    }
160    fn flush(&mut self) -> IOResult<()> {
161        WriteImmut::flush(self)
162    }
163}
164
165#[derive(Debug, Default)]
166pub struct WritableLogLocationTargetMap(HashMap<Level, SmallVec<[WritableLogLocationTarget; 6]>>);
167
168impl Display for WritableLogLocationTargetMap {
169    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
170        f.write_fmt(format_args!("{:#?}", self))
171    }
172}
173
174struct FileLogger {
175    targets: RwLock<WritableLogLocationTargetMap>,
176    name: Lazy<&'static str>,
177    file_prefix: Lazy<(String, String)>,
178}
179
180impl FileLogger {
181    fn clear_targets(&self) {
182        use std::borrow::BorrowMut;
183
184        if let Ok(mut targets) = self.targets.write() {
185            let prev_map: WritableLogLocationTargetMap = std::mem::replace(
186                targets.borrow_mut(),
187                WritableLogLocationTargetMap(HashMap::new()),
188            );
189
190            drop(prev_map);
191        }
192    }
193}
194
195struct LevelStringStruct {
196    error: String,
197    warn: String,
198    info: String,
199    debug: String,
200    trace: String,
201}
202
203impl LevelStringStruct {
204    fn get_level(&self, level: Level) -> &str {
205        use Level::*;
206
207        match level {
208            Error => &self.error,
209            Warn  => &self.warn,
210            Info  => &self.info,
211            Debug => &self.debug,
212            Trace => &self.trace,
213        }
214    }
215}
216
217impl Default for LevelStringStruct {
218    fn default() -> Self {
219        Self {
220            error: color_text_fmt!(bold "31", "{:<5}", "ERROR").to_string(),
221            warn:  color_text_fmt!(bold "33", "{:<5}", "WARN ").to_string(),
222            info:  color_text_fmt!(bold "36", "{:<5}", "INFO ").to_string(),
223            debug: color_text_fmt!(bold "32", "{:<5}", "DEBUG").to_string(),
224            trace: color_text_fmt!(bold "35", "{:<5}", "TRACE").to_string(),
225        }
226    }
227}
228
229lazy_static! {
230    static ref LEVEL_STRINGS: LevelStringStruct = LevelStringStruct::default();
231}
232
233impl log::Log for FileLogger {
234    fn enabled(&self, metadata: &Metadata) -> bool {
235        Some(&metadata.target()) == self.name.get()
236    }
237
238    fn log(&self, record: &Record) {
239        if !self.enabled(record.metadata()) { return; }
240        let target_map = match self.targets.read() {
241            Ok(guard) => guard,
242            Err(e) => {
243                eprintln!("Logging target poisoned! {}", e);
244                return;
245            }
246        };
247
248        let utc: DateTime<Utc> = Utc::now();
249        let (level, args) = (record.level(), record.args());
250
251        for target in target_map.0.get(&record.level()).into_iter().flatten() {
252            let stripped = with_path_prefix_stripped(record.file(), self.file_prefix.get());
253            let full: Option<String> = stripped.map(|(prefix, body)| [prefix, body].into_iter().collect());
254
255            let log_result = logging_parts!(
256                target; <==
257                "{} "   - Some(color_text_fmt!("38;5;147", "{}", utc.format("%b %d"))),
258                "{}"    - Some(color_text_fmt!("38;5;86", "{}", utc.format("%H:%M:%S"))),
259                "{} | " - Some(color_text_fmt!("38;5;23", "{}", utc.format("%.3f"))),
260
261                "{} "   - color_text_fmt!(option "38;5;47", "{}", record.module_path()),
262                
263                "{}" - color_text_fmt!(option "38;5;159", "{}", full.as_ref())
264                    => ":{}" - color_text_fmt!(option "38;5;159", "{:<3}", record.line())
265                        => * "; ",
266                "{} - " - Some(LEVEL_STRINGS.get_level(level)),
267                "{}" - Some(args),
268            );
269
270            if let Err(error) = log_result {
271                eprintln!("Failed to log to x! Error: {:?}", error);
272            }
273        }
274    }
275
276    fn flush(&self) {
277        let target_map = match self.targets.read() {
278            Ok(guard) => guard,
279            Err(e) => {
280                eprintln!("Logging target poisoned! {}", e);
281                return;
282            }
283        };
284
285        for target in target_map.0.values().flatten() {
286            if let Err(error) = target.flush() {
287                eprintln!("Failed to flush to x! Error: {:?}", error);
288            }
289        }
290    }
291}
292
293lazy_static! {
294    static ref LOGGER: FileLogger = FileLogger {
295        targets: RwLock::default(),
296        name: Lazy::new(),
297        file_prefix: Lazy::new(),
298    };
299}
300
301pub fn with_path_prefix_stripped<'a>(path: Option<&'a str>, prefix: Option<&'a (String, String)>) -> Option<(&'a str, &'a str)> {
302    if let (Some(path), Some((prefix, replace))) = (path, prefix) {
303        path.strip_prefix(prefix).map_or_else(
304            || Some(("", path)),
305            |stripped| Some((replace, stripped)),
306        )
307    } else {
308        path.map(|p| ("", p))
309    }
310}
311
312pub fn set_up_logging(input: &LogLocationTargetMap<'_>, name: &'static str) -> IOResult<impl FnOnce()> {
313    LOGGER.name.get_or_create(|| name);
314    if let Ok(value) = std::env::var("LOGGING_PREFIX_REPLACE") {
315        if let Some((prefix, replace)) = value.split_once("->") {
316            LOGGER.file_prefix.get_or_create(|| (prefix.to_string(), replace.to_string()));
317        }
318    }
319
320    let mut target_hashmap = LOGGER
321        .targets
322        .write()
323        .map_err(|error| IOError::new(ErrorKind::Other, PoisonErrorWrapper::from(error)))?;
324
325    *target_hashmap = generate_writable_log_location_target_map(input, Utc::now());
326
327    let log_handler = signals::setup_signal_handler()?;
328
329    log::set_logger(&*LOGGER)
330        .map(|()| log::set_max_level(LevelFilter::Trace))
331        .map_err(|error| IOError::new(ErrorKind::Other, ErrorWrapper::from(error)))?;
332
333    Ok(log_handler)
334}
335
336pub fn generate_writable_log_location_target_map(
337    from: &LogLocationTargetMap,
338    time_startup: DateTime<Utc>,
339) -> WritableLogLocationTargetMap {
340    let mut file_map = HashMap::new();
341    let mut webwriter_map = HashMap::new();
342    WritableLogLocationTargetMap(
343        from.iter()
344            .map(|(level, targets)| {
345                let mut writable_targets = vec![];
346
347                for target in targets {
348                    let writable_target = match target {
349                        LogLocationTarget::File(path, size_limit) if !file_map.contains_key(path) => 'result: {
350                            let file = match OpenOptions::new().write(true).read(true).create(true).open(path) {
351                                Ok(file) => file,
352                                Err(e) => {
353                                    eprintln!("Failed to open file! Error: {}", e);
354                                    break 'result Err(IOError::new(ErrorKind::Other, e));
355                                }
356                            };
357
358                            let mut circular_file = match CircularFileBuffer::new(file, size_limit.unwrap_or(u64::MAX)) {
359                                Ok(circular_file) => circular_file,
360                                Err(e) => {
361                                    eprintln!("Failed to create circular file buffer! Error: {:?}", e);
362                                    break 'result Err(IOError::new(ErrorKind::Other, e));
363                                }
364                            };
365                            let write_result = writeln!(
366                                circular_file,
367                                "\n\n{:-^50}",
368                                format!(
369                                    "Logging started at {}",
370                                    time_startup.format("%b %d %H:%M:%S%.3f"),
371                                ),
372                            );
373                            if let Err(e) = write_result {
374                                eprintln!("Failed to write to circular file buffer! Error: {}", e);
375                                break 'result Err(IOError::new(ErrorKind::Other, e));
376                            }
377                            let new_file = Arc::new(RwLock::new(circular_file));
378                            let new_file = file_map.entry(path).or_insert(new_file.clone()).clone();
379                            Ok(WritableLogLocationTarget::File(new_file))
380                        }
381                        LogLocationTarget::File(path, _) => match file_map.get(path) {
382                            Some(file) => Ok(WritableLogLocationTarget::File(file.clone())),
383                            None => Err(std::io::Error::new(
384                                ErrorKind::Other,
385                                format!("File not found in file_map! {:?}", path),
386                            )),
387                        },
388                        LogLocationTarget::WebWriter(url) => 'result: {
389                            let url = match Url::parse(url) {
390                                Ok(url) => url,
391                                Err(e) => {
392                                    eprintln!("Failed to parse url! Error: {}", e);
393                                    break 'result Err(IOError::new(ErrorKind::Other, e));
394                                }
395                            };
396                            let web_writer = webwriter_map
397                                .entry(url.clone())
398                                .or_insert_with(|| Arc::new(WebWriter::new(url.clone())))
399                                .clone();
400
401                            Ok(WritableLogLocationTarget::WebWriter(web_writer))
402                        }
403                        LogLocationTarget::StdOut => Ok(WritableLogLocationTarget::StdOut),
404                        LogLocationTarget::StdErr => Ok(WritableLogLocationTarget::StdErr),
405                    };
406
407
408                    match writable_target {
409                        Ok(writable_target) => writable_targets.push(writable_target),
410                        Err(error) => eprintln!("{}", error),
411                    }
412                }
413
414                (*level, smallvec::SmallVec::from_vec(writable_targets))
415            })
416            .collect(),
417    )
418}
419
420
421lazy_static! {
422    pub static ref ERR_FILE: &'static Path = Path::new("./err.log");
423    pub static ref ERR_WARN_FILE: &'static Path = Path::new("./err_warn.log");
424    pub static ref INFO_DEBUG_FILE: &'static Path = Path::new("./info_debug.log");
425    pub static ref ALL_LOG_FILE: &'static Path = Path::new("./all.log");
426
427    pub static ref LOGGING_URL: Option<String> = {
428        if let Ok(url) = std::env::var("LOGGING_TARGET_URL") {
429            if let Ok(url) = Url::parse(&url) {
430                Some(url.to_string())
431            } else { None }
432        } else { None }
433    };
434    
435    pub static ref DEFAULT_LOGGING_TARGETS: LogLocationTargetMap<'static> = {
436        use Level::*;
437        use LogLocationTarget::*;
438
439
440        let mut target_map = vec![
441            (Trace, smallvec![
442                StdOut,
443                File(&ALL_LOG_FILE, None),
444            ]),
445            (Debug, smallvec![
446                // StdOut,
447                File(&INFO_DEBUG_FILE, None),
448                File(&ALL_LOG_FILE, None),
449            ]),
450            (Info, smallvec![
451                StdOut,
452                File(&INFO_DEBUG_FILE, None),
453                File(&ALL_LOG_FILE, None),
454            ]),
455            (Warn, smallvec![
456                StdErr,
457                File(&ERR_WARN_FILE, None),
458                File(&ALL_LOG_FILE, None),
459            ]),
460            (Error, smallvec![
461                StdErr,
462                File(&ERR_FILE, None),
463                File(&ERR_WARN_FILE, None),
464                File(&ALL_LOG_FILE, None),
465            ]),
466
467        ];
468
469        if let Some(url) = LOGGING_URL.as_ref() {
470            target_map[0].1.push(WebWriter(url));
471            target_map[1].1.push(WebWriter(url));
472            target_map[2].1.push(WebWriter(url));
473            target_map[3].1.push(WebWriter(url));
474            target_map[4].1.push(WebWriter(url));
475        }
476
477        target_map.into_iter().collect()
478    };
479}
480
481pub fn default_logging_targets_with_size_limit(limit: u64) -> LogLocationTargetMap<'static> {
482    use Level::*;
483    use LogLocationTarget::*;
484
485
486    let mut target_map = vec![
487        (Trace, smallvec![
488            StdOut,
489            File(&ALL_LOG_FILE, Some(limit)),
490        ]),
491        (Debug, smallvec![
492            // StdOut,
493            File(&INFO_DEBUG_FILE, Some(limit)),
494            File(&ALL_LOG_FILE, Some(limit)),
495        ]),
496        (Info, smallvec![
497            StdOut,
498            File(&INFO_DEBUG_FILE, Some(limit)),
499            File(&ALL_LOG_FILE, Some(limit)),
500        ]),
501        (Warn, smallvec![
502            StdErr,
503            File(&ERR_WARN_FILE, Some(limit)),
504            File(&ALL_LOG_FILE, Some(limit)),
505        ]),
506        (Error, smallvec![
507            StdErr,
508            File(&ERR_FILE, Some(limit)),
509            File(&ERR_WARN_FILE, Some(limit)),
510            File(&ALL_LOG_FILE, Some(limit)),
511        ]),
512
513    ];
514
515    if let Some(url) = LOGGING_URL.as_ref() {
516        target_map[0].1.push(WebWriter(url));
517        target_map[1].1.push(WebWriter(url));
518        target_map[2].1.push(WebWriter(url));
519        target_map[3].1.push(WebWriter(url));
520        target_map[4].1.push(WebWriter(url));
521    }
522
523    target_map.into_iter().collect()
524}