zarthus_env_logger/
lib.rs1use env_logger::fmt::{Color, Formatter};
2use log::{Level, LevelFilter, Record};
3use std::env;
4
5#[cfg(all(feature = "chrono", feature = "time"))]
6compile_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
33fn 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 #[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}