my_env_logger_style/lib.rs
1#![allow(clippy::tabs_in_doc_comments)]
2#![warn(rust_2018_idioms, unreachable_pub)]
3#![forbid(elided_lifetimes_in_paths)]
4#![cfg_attr(all(doc, nightly), feature(doc_auto_cfg))]
5
6//! A pretty, opinionated style for [env_logger](https://crates.io/crates/env_logger) inspirated by [pretty-env-logger](https://crates.io/crates/pretty_env_logger).
7//!
8//! It is not a goal of this crate to create a feature rich wrapper around [env_logger](https://crates.io/crates/env_logger).
9//! Instead it does provide a formater, which can be applied to the [`env_logger::Builder`].
10//! Additional an optional [function](just_log) to create and register a zero config logger is provided.
11//!
12//! Timestamp, emojis and modules can be disable separately.
13//!
14//! # Preview
15//!
16//! 
17//!
18//! with timestamps:
19//!
20//! 
21//! # Usage
22//! #### Quickstart
23//! ```
24//! # use log::info;
25//! my_env_logger_style::just_log();
26//! info!("Hello, world!");
27//! ```
28//! This creates the default env_logger from environment variables and register it as logger.
29//! #### Advance
30//! You can also create an [`env_logger::Builder`] and apply the style definded at this crate,
31//! by using the [`format()`] function.
32//! ```
33//! use log::info;
34//! use my_env_logger_style::format;
35//!
36//! env_logger::Builder::new()
37//! .parse_default_env()
38//! .format(format)
39//! .init();
40//! info!("Hello, world!");
41//! ```
42//! # Feature-flags
43//! #### time (default)
44//! Enable RFC3339 timestamps
45//! #### custom-arg-formatter
46//! Allow using a custom formater to format the args (the actual message) of the log record.
47//! As example this can be used to avoid logging private userdata.
48use anstyle::Style;
49use env_logger::fmt::Formatter;
50use log::{Level, Record};
51#[cfg(feature = "custom-arg-formatter")]
52use once_cell::sync::OnceCell;
53use std::{
54 io,
55 io::Write,
56 sync::atomic::{AtomicBool, AtomicU8, AtomicUsize, Ordering}
57};
58
59static MAX_MODULE_LEN: AtomicUsize = AtomicUsize::new(0);
60static SHOW_MODULE: AtomicBool = AtomicBool::new(true);
61static SHOW_EMOJIS: AtomicBool = AtomicBool::new(true);
62#[cfg(feature = "time")]
63static SHOW_TIME: AtomicU8 = AtomicU8::new(TimestampPrecision::Seconds as u8);
64#[cfg(feature = "custom-arg-formatter")]
65static ARG_FORMATTER: OnceCell<Box<dyn ArgFormatter + Send + Sync>> = OnceCell::new();
66
67pub use env_logger;
68
69#[repr(u8)]
70/// RFC3339 timestamps
71pub enum TimestampPrecision {
72 Disable,
73 Seconds,
74 Millis,
75 Micros,
76 Nanos
77}
78
79/// Create a preconfigured builder,
80/// with same configuration like [`just_log()`].
81///
82/// For an unconfigurated bulider use [`env_logger::Builder::new()`]
83pub fn builder() -> env_logger::Builder {
84 let mut builder = env_logger::Builder::new();
85 builder.filter_level(log::LevelFilter::Info) //set defaul log level
86 .parse_default_env()
87 .format(format);
88 builder
89}
90
91///create and regstier a logger from the default environment variables
92pub fn just_log() {
93 builder().init();
94}
95
96///enable or disabel showing the module path
97pub fn show_module(show: bool) {
98 SHOW_MODULE.store(show, Ordering::Relaxed);
99}
100
101///enable or disabel showing emojis before the log level
102pub fn show_emoji(show: bool) {
103 SHOW_EMOJIS.store(show, Ordering::Relaxed);
104}
105
106/// return the current module len and set the module length to the maximum of the current value and the given `len`.
107///
108/// Usefull if you already know the length of module and would like to have an consistant indentation from the beginnig.
109pub fn get_set_max_module_len(len: usize) -> usize {
110 let module_len = MAX_MODULE_LEN.load(Ordering::Relaxed);
111 if module_len < len {
112 MAX_MODULE_LEN.store(len, Ordering::Relaxed);
113 }
114 module_len
115}
116
117#[cfg(feature = "time")]
118/// set the timestamp precision or disable timestamps complete
119pub fn set_timestamp_precision(timestamp_precission: TimestampPrecision) {
120 SHOW_TIME.store(timestamp_precission as u8, Ordering::Relaxed);
121}
122
123pub trait ArgFormatter {
124 fn arg_format(&self, buf: &mut Formatter, record: &Record<'_>) -> io::Result<()>;
125}
126
127impl<F: Fn(&mut Formatter, &Record<'_>) -> io::Result<()>> ArgFormatter for F {
128 fn arg_format(&self, buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> {
129 self(buf, record)
130 }
131}
132
133/// Use a custom formater to format the args (the actual message) of the record .
134/// This function can only be called once and return an Error if called a second time.
135///
136/// This example remove the private user ipv4 loggeg by `hickory` from the log, if the loglevel is below Debug.
137/// ```
138/// use env_logger::fmt::Formatter;
139/// use log::{Level, Record};
140/// use once_cell::sync::Lazy;
141/// use regex::Regex;
142/// use std::{io, io::Write};
143///
144/// static REGEX: Lazy<Regex> = Lazy::new(|| {
145/// // https://ihateregex.io/expr/ip/
146/// Regex::new(r"(\b25[0-5]|\b2[0-4][0-9]|\b[01]?[0-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}")
147/// .unwrap()
148/// });
149///
150/// fn arg_format(buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> {
151/// if let Some(mod_path) = record.module_path() {
152/// if log::max_level() < Level::Debug && mod_path.starts_with("hickory") {
153/// let message = format!("{}", record.args());
154/// let message = REGEX.replace_all(&message, "RESTRAINED");
155/// return writeln!(buf, "{}", message);
156/// }
157/// };
158/// writeln!(buf, "{}", record.args())
159/// }
160///
161/// my_env_logger_style::set_arg_formatter(Box::new(arg_format)).unwrap();
162/// ```
163#[cfg(feature = "custom-arg-formatter")]
164pub fn set_arg_formatter(
165 forrmatter: Box<dyn ArgFormatter + Send + Sync>
166) -> Result<(), ()> {
167 ARG_FORMATTER.set(forrmatter).map_err(|_| ())
168}
169
170///log formater witch can be used at the [`format()`](env_logger::Builder::format()) function of the [`env_logger::Builder`].
171pub fn format(buf: &mut Formatter, record: &Record<'_>) -> io::Result<()> {
172 let bold = Style::new().bold();
173 let dimmed = Style::new().dimmed();
174
175 #[cfg(feature = "time")]
176 {
177 let show_time = SHOW_TIME.load(Ordering::Relaxed);
178 // safety: SHOW_TIME is inilized with TimestampPrecision::Seconds
179 // and can only be written by using set_timestamp_precision()
180 match unsafe { std::mem::transmute(show_time) } {
181 TimestampPrecision::Disable => Ok(()),
182 TimestampPrecision::Seconds => {
183 write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_seconds())
184 },
185 TimestampPrecision::Millis => {
186 write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_millis())
187 },
188 TimestampPrecision::Micros => {
189 write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_micros())
190 },
191 TimestampPrecision::Nanos => {
192 write!(buf, "{dimmed}{}{dimmed:#} ", buf.timestamp_nanos())
193 }
194 }?;
195 }
196
197 let level_style = buf.default_level_style(record.level());
198 let level_symbol = if SHOW_EMOJIS.load(Ordering::Relaxed) {
199 match record.level() {
200 //💥 and 🔬 are 2 chars big at the terminal. How does it look with other fonts/terminals?
201 Level::Trace => "🔬",
202 Level::Debug => " ⚙️",
203 Level::Info => " ℹ",
204 Level::Warn => " ⚠",
205 Level::Error => "💥"
206 }
207 } else {
208 ""
209 };
210 write!(
211 buf,
212 "{level_symbol} {level_style}{:5}{level_style:#} ",
213 record.level()
214 )?;
215
216 if SHOW_MODULE.load(Ordering::Relaxed) {
217 let module = record.module_path().unwrap_or_default();
218 let module_len = get_set_max_module_len(module.len());
219 write!(
220 buf,
221 "{dimmed}{module:module_len$}{dimmed:#} {bold}>{bold:#} "
222 )?;
223 }
224
225 #[cfg(feature = "custom-arg-formatter")]
226 if let Some(formatter) = ARG_FORMATTER.get() {
227 return formatter.arg_format(buf, record);
228 }
229 writeln!(buf, "{}", record.args())
230}