zarthus_env_logger/
lib.rs

1use env_logger::fmt::{Color, Formatter};
2use log::{Level, LevelFilter, Record};
3use std::env;
4
5#[cfg(all(feature = "chrono", feature = "time"))]
6// You will want to either `--no-default-features --features="time"` or not enable the time feature to default to chrono.
7compile_error!("Feature 'chrono' and 'time' are mutually exclusive and cannot be enabled together");
8
9pub fn init() {
10    init_custom(vec![env!("CARGO_PKG_NAME")], LevelFilter::Debug, timestamp_format())
11}
12
13pub fn init_named(module_name: &'static str) {
14    init_custom(vec![module_name], LevelFilter::Debug, timestamp_format())
15}
16
17pub fn init_named_many(module_names: Vec<&'static str>) {
18    init_custom(module_names, LevelFilter::Debug, timestamp_format())
19}
20
21pub fn init_custom(
22    module_names: Vec<&'static str>,
23    level_filter: LevelFilter,
24    timestamp_format: &'static str,
25) {
26    if env::var("RUST_LOG").is_ok() {
27        env_logger::init();
28    } else {
29        builder(&module_names, level_filter, timestamp_format)
30    }
31}
32
33/// Initializes the env_logger through a custom builder
34///
35/// params:
36///   module_name: the name of the package
37///   level_filter: the level to filter for your package
38///   timestamp_format: if the `chrono` or `time` feature is enabled, format timestamps like this
39///
40/// Does not return any value.
41fn builder(
42    module_names: &Vec<&'static str>,
43    level_filter: LevelFilter,
44    timestamp_format: &'static str,
45) {
46    let first_module_name = module_names.first().unwrap();
47
48    let mut builder = env_logger::builder();
49
50    for module_name in module_names {
51        builder.filter_module(module_name, level_filter);
52    }
53
54    builder
55        .filter(None, LevelFilter::Error)
56        .format_level(true)
57        .format(|buf, record| format(buf, record, first_module_name, timestamp_format))
58        .init();
59
60    log::trace!("RUST_LOG was not set, using self-built debug logger");
61}
62
63fn format(
64    buf: &mut Formatter,
65    record: &Record,
66    module_name: &str,
67    timestamp_format: &str,
68) -> std::io::Result<()> {
69    use std::io::Write;
70
71    let _target = record.target();
72
73    let mut style = buf.style();
74    let level = match record.level() {
75        Level::Trace => style.set_color(Color::Magenta).value("TRACE"),
76        Level::Debug => style.set_color(Color::Blue).value("DEBUG"),
77        Level::Info => style.set_color(Color::Green).value("INFO "),
78        Level::Warn => style.set_color(Color::Yellow).value("WARN "),
79        Level::Error => style.set_color(Color::Red).value("ERROR"),
80    };
81
82    let mut style = buf.style();
83    let target = style.set_bold(true).value(record.target());
84
85    let mut style = buf.style();
86    let ts = buf.timestamp().to_string();
87    let time = style
88        .set_color(Color::Rgb(128, 128, 128))
89        .value(format_timestamp(ts, timestamp_format));
90
91    writeln!(
92        buf,
93        "{} {} {} > {}",
94        time,
95        level,
96        target.to_string().replace(module_name, "@"),
97        record.args()
98    )
99}
100
101fn timestamp_format() -> &'static str {
102    #[cfg(feature = "time")]
103    return "[hour]:[minute]:[second]";
104    #[cfg(not(feature = "time"))]
105    "%H:%M:%S"
106}
107#[cfg(all(feature = "chrono", not(feature = "time")))]
108fn format_timestamp(ts: String, timestamp_format: &str) -> String {
109    chrono::DateTime::parse_from_rfc3339(&ts)
110        .map(|dt| dt.format(timestamp_format).to_string())
111        .unwrap_or_else(|_| ts)
112}
113
114#[cfg(all(feature = "time", not(feature = "chrono")))]
115fn format_timestamp(ts: String, timestamp_format: &str) -> String {
116    use ::time::OffsetDateTime;
117    use ::time::format_description::well_known::Rfc3339;
118
119    let fmt = ::time::format_description::parse(timestamp_format).unwrap();
120
121    let parsed_result = OffsetDateTime::parse(&ts, &Rfc3339);
122    match parsed_result {
123        Ok(parsed) => return parsed.format(&fmt).unwrap_or_else(|_| ts),
124        Err(_) => ts,
125    }
126}
127
128#[cfg(all(not(feature = "chrono"), not(feature = "time")))]
129fn format_timestamp(ts: String, _timestamp_format: &str) -> String {
130    ts
131}
132
133
134#[cfg(test)]
135mod tests {
136    use crate::{builder, format_timestamp, timestamp_format};
137
138    #[test]
139    fn test_builder() {
140        let modules_names = vec!["foo"];
141
142        builder(&modules_names, log::LevelFilter::Error, "%H:M:%S");
143    }
144
145    // #[test]
146    // fn test_format() {
147    //     let mut formatter: &mut Formatter;
148    //
149    //     let record = Record::builder()
150    //         .level(log::Level::Info)
151    //         .file(Some("foo"))
152    //         .module_path(Some("modulepath"))
153    //         .build();
154    //
155    //     let output = format(formatter, &record, "modulepath", "%H:%M")
156    //         .expect("This should not have happened.");
157    //
158    //     assert_eq!("Foo", output)
159    // }
160
161    #[cfg(any(feature = "chrono", feature = "time"))]
162    #[test]
163    fn test_format_timestamp() {
164        let fmt = timestamp_format();
165
166        assert_eq!(
167            "20:05:05",
168            format_timestamp("2023-12-31T20:05:05+02:00".to_string(), fmt),
169        );
170    }
171
172    #[cfg(any(feature = "chrono", feature = "time"))]
173    #[test]
174    fn test_format_timestamp_crashes_on_bad_format() {
175        assert_eq!("", format_timestamp("".to_string(), "%9 ABC"));
176    }
177
178    #[cfg(all(not(feature = "chrono"), not(feature = "time")))]
179    #[test]
180    fn test_format_timestamp() {
181        assert_eq!(
182            "1000",
183            format_timestamp("1000".to_string(), "%H:%M:%S %Y %Z"),
184        );
185    }
186}