use core::fmt::{Debug, Formatter};
use log::{LevelFilter, Record};
use std::io::{Error, Write};
use std::sync::Arc;
use termcolor::{BufferedStandardStream, Color, ColorSpec};
pub use time::format_description::FormatItem;
pub use time::macros::format_description;
pub use time::UtcOffset;
const LEVEL_NUMBER: usize = 6;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum TimeFormat {
Rfc2822, Rfc3339, TimeWithMicro, DateTimeWithMicro, Custom(&'static [FormatItem<'static>]),
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum Token {
Text(&'static str),
Time,
Level,
ColorStart,
ColorEnd,
ThreadId,
ThreadName,
ProcessId,
Module,
FileName,
File,
Line,
Message,
}
type FilterFunction = dyn Fn(&Record) -> bool + Send + Sync;
type WriteFunction = dyn Fn(&Record, &mut dyn Write) -> Result<(), Error> + Send + Sync;
type TerminalWriteFunction = dyn Fn(&Record, &mut BufferedStandardStream) -> Result<(), Error> + Send + Sync;
#[derive(Clone)]
pub struct Config {
pub(crate) level: LevelFilter,
pub(crate) time_offset: UtcOffset,
pub(crate) write_once: bool,
pub(crate) time_format: [TimeFormat; LEVEL_NUMBER],
pub(crate) format_text: [&'static str; LEVEL_NUMBER],
pub(crate) tokens: [Vec<Token>; LEVEL_NUMBER],
pub(crate) colored_text_color: [Option<Color>; LEVEL_NUMBER],
pub(crate) background_color: [Option<Color>; LEVEL_NUMBER],
pub(crate) compiled_colors: [ColorSpec; LEVEL_NUMBER],
pub(crate) enabled_colors: bool,
pub(crate) message_filtering: Option<Arc<FilterFunction>>,
pub(crate) write_formatter: Option<Arc<WriteFunction>>,
pub(crate) terminal_formatter: Option<Arc<TerminalWriteFunction>>,
}
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum FormatText {
MessageOnly,
MessageOnlyC,
LevelMessage,
LevelMessageC,
Simple,
SimpleC,
Default,
DefaultC,
DefaultWithFileName,
DefaultWithFileNameC,
DefaultWithThread,
DefaultWithThreadC,
DefaultWithThreadFile,
DefaultWithThreadFileC,
}
impl FormatText {
pub fn get(&self) -> &'static str {
match self {
Self::MessageOnly => "[_msg]",
Self::MessageOnlyC => "[_color_start][_msg][_color_end]",
Self::LevelMessage => "[[_level]] [_msg]",
Self::LevelMessageC => "[_color_start][[_level]][_color_end] [_msg]",
Self::Simple => "[_time] [[_level]] [_msg]",
Self::SimpleC => "[_time] [_color_start][[_level]][_color_end] [_msg]",
Self::Default => "[_time] [[_level]] [_module]: [_msg]",
Self::DefaultC => "[_time] [_color_start][[_level]][_color_end] [_module]: [_msg]",
Self::DefaultWithFileName => "[_time] [[_level]] [[_module]] [_file_name]:[_line] - [_msg]",
Self::DefaultWithFileNameC => "[_time] [_color_start][[_level]][_color_end] [[_module]] [_file_name]:[_line] - [_msg]",
Self::DefaultWithThread => "[_time] [[_level]] [[_module]] ([_thread_id]) - [_msg]",
Self::DefaultWithThreadC => "[_time] [_color_start][[_level]][_color_end] ([_thread_id]) [[_module]] - [_msg]",
Self::DefaultWithThreadFile => "[_time] [[_level]] [[_module]] ([_thread_id]) [_file_name]:[_line] - [_msg]",
Self::DefaultWithThreadFileC => "[_time] [_color_start][[_level]][_color_end] ([_thread_id]) [[_module]] [_file_name]:[_line] - [_msg]",
}
}
}
impl Config {
pub(crate) fn calculate_data(&mut self) {
self.calculate_tokens();
self.calculate_colors();
}
fn calculate_colors(&mut self) {
for (idx, color_spec) in self.compiled_colors.iter_mut().enumerate() {
*color_spec = ColorSpec::new().set_bg(self.background_color[idx]).set_fg(self.colored_text_color[idx]).clone();
}
}
fn calculate_tokens(&mut self) {
let allowed_tokens = [
("[_time]", Token::Time),
("[_level]", Token::Level),
("[_color_start]", Token::ColorStart),
("[_color_end]", Token::ColorEnd),
("[_thread_id]", Token::ThreadId),
("[_thread_name]", Token::ThreadName),
("[_process_id]", Token::ProcessId),
("[_module]", Token::Module),
("[_file]", Token::File),
("[_file_name]", Token::FileName),
("[_line]", Token::Line),
("[_msg]", Token::Message),
];
for (idx, format_text) in self.format_text.into_iter().enumerate() {
let mut collected_tokens = Vec::new();
let mut current_index = 0;
loop {
let mut minimum_index = usize::MAX;
let mut choose_token_text = None;
for (token_txt, token) in &allowed_tokens {
if let Some(find_idx) = format_text[current_index..].find(token_txt) {
if minimum_index > find_idx + current_index {
minimum_index = find_idx + current_index;
choose_token_text = Some((token_txt, token));
}
}
}
if let Some((token_txt, token)) = choose_token_text {
if minimum_index > current_index {
let text = &format_text[current_index..minimum_index];
collected_tokens.push(Token::Text(text));
}
collected_tokens.push(token.clone());
current_index = minimum_index + token_txt.len() - 1;
} else {
if current_index != format_text.len() {
let text = &format_text[current_index..];
collected_tokens.push(Token::Text(text));
}
break; }
current_index += 1;
}
self.tokens[idx] = collected_tokens;
}
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder(Config);
impl ConfigBuilder {
#[must_use]
pub fn new() -> Self {
Self(Config::default())
}
pub fn set_format_text(&mut self, format_text: &'static str, level: Option<LevelFilter>) -> &mut Self {
if let Some(level) = level {
self.0.format_text[level as usize] = format_text;
} else {
self.0.format_text = [format_text; LEVEL_NUMBER];
}
self
}
pub fn set_background_color(&mut self, background_color: Option<Color>, level: Option<LevelFilter>) -> &mut Self {
if let Some(level) = level {
self.0.background_color[level as usize] = background_color;
} else {
self.0.background_color = [background_color; LEVEL_NUMBER];
}
self
}
pub fn set_colored_text_color(&mut self, colored_text_color: Option<Color>, level: Option<LevelFilter>) -> &mut Self {
if let Some(level) = level {
self.0.colored_text_color[level as usize] = colored_text_color;
} else {
self.0.colored_text_color = [colored_text_color; LEVEL_NUMBER];
}
self
}
pub fn set_enabled_colours(&mut self, enabled_colours: bool) -> &mut Self {
self.0.enabled_colors = enabled_colours;
self
}
pub fn set_level(&mut self, level: LevelFilter) -> &mut Self {
self.0.level = level;
self
}
pub fn set_write_once(&mut self, write_once: bool) -> &mut Self {
self.0.write_once = write_once;
self
}
pub fn set_time_format(&mut self, time_format: TimeFormat, level: Option<LevelFilter>) -> &mut Self {
if let Some(level) = level {
self.0.time_format[level as usize] = time_format;
} else {
self.0.time_format = [time_format; LEVEL_NUMBER];
}
self
}
pub fn set_time_offset(&mut self, offset: UtcOffset) -> &mut Self {
self.0.time_offset = offset;
self
}
pub fn set_time_offset_to_local(&mut self) -> Result<&mut Self, &mut Self> {
match Self::get_local_time_offset() {
Some(offset) => {
self.0.time_offset = offset;
Ok(self)
}
None => Err(self),
}
}
pub fn set_remove_time_offset(&mut self) -> &mut Self {
self.0.time_offset = UtcOffset::UTC;
self
}
pub fn set_message_filtering<F>(&mut self, message_filtering: Option<F>) -> &mut Self
where
F: Fn(&Record) -> bool + Send + Sync + 'static,
{
if let Some(message_filtering) = message_filtering {
self.0.message_filtering = Some(Arc::new(message_filtering));
} else {
self.0.message_filtering = None;
}
self
}
pub fn set_custom_write_formatter<F>(&mut self, write_formatter: Option<F>) -> &mut Self
where
F: Fn(&Record, &mut dyn Write) -> Result<(), Error> + Send + Sync + 'static,
{
if let Some(write_formatter) = write_formatter {
self.0.write_formatter = Some(Arc::new(write_formatter));
} else {
self.0.write_formatter = None;
}
self
}
pub fn set_custom_terminal_formatter<F>(&mut self, terminal_formatter: Option<F>) -> &mut Self
where
F: Fn(&Record, &mut BufferedStandardStream) -> Result<(), Error> + Send + Sync + 'static,
{
if let Some(terminal_formatter) = terminal_formatter {
self.0.terminal_formatter = Some(Arc::new(terminal_formatter));
} else {
self.0.terminal_formatter = None;
}
self
}
fn get_local_time_offset() -> Option<UtcOffset> {
#[cfg(not(feature = "timezone_file_access"))]
{
return None;
}
#[cfg(feature = "timezone_file_access")] {
#[cfg(target_family = "unix")]
{
let Ok(timezone) = tz::TimeZone::local() else {
return None;
};
let Ok(time_type) = timezone.find_current_local_time_type() else {
return None;
};
UtcOffset::from_whole_seconds(time_type.ut_offset()).ok()
}
#[cfg(not(target_family = "unix"))]
{
time::UtcOffset::current_local_offset().ok()
}
}
}
pub fn build(&mut self) -> Config {
self.0.clone()
}
}
impl Default for ConfigBuilder {
fn default() -> Self {
Self::new()
}
}
impl Default for Config {
fn default() -> Self {
let tz_offset = ConfigBuilder::get_local_time_offset().unwrap_or(UtcOffset::UTC);
Self {
level: LevelFilter::Info,
write_once: false,
time_format: [TimeFormat::TimeWithMicro; LEVEL_NUMBER],
time_offset: tz_offset,
tokens: [vec![], vec![], vec![], vec![], vec![], vec![]],
colored_text_color: [
None,
Some(Color::Red), Some(Color::Yellow), Some(Color::Blue), Some(Color::Cyan), Some(Color::White), ],
background_color: [None, None, None, None, None, None],
enabled_colors: true,
format_text: [FormatText::DefaultC.get(); LEVEL_NUMBER],
compiled_colors: [ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new(), ColorSpec::new()],
message_filtering: None,
write_formatter: None,
terminal_formatter: None,
}
}
}
impl Debug for Config {
fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
f.debug_struct("Config")
.field("level", &self.level)
.field("write_once", &self.write_once)
.field("time_format", &self.time_format)
.field("time_offset", &self.time_offset)
.field("tokens", &self.tokens)
.field("colored_text_color", &self.colored_text_color)
.field("background_color", &self.background_color)
.field("enabled_colors", &self.enabled_colors)
.field("format_text", &self.format_text)
.field("compiled_colors", &self.compiled_colors)
.finish_non_exhaustive()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test() {
let text = "[_time] [_level] [_thread_id] [_thread_name] [_process_id] [_module] [_file][_file_name] [_line] [_color_start][_msg][_color_end] [RAR]";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(
config.tokens[0],
vec![
Token::Time,
Token::Text(" "),
Token::Level,
Token::Text(" "),
Token::ThreadId,
Token::Text(" "),
Token::ThreadName,
Token::Text(" "),
Token::ProcessId,
Token::Text(" "),
Token::Module,
Token::Text(" "),
Token::File,
Token::FileName,
Token::Text(" "),
Token::Line,
Token::Text(" "),
Token::ColorStart,
Token::Message,
Token::ColorEnd,
Token::Text(" [RAR]"),
]
);
let text = "]]][[";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(config.tokens[0], vec![Token::Text("]]][[")]);
let text = " [_time]";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(config.tokens[0], vec![Token::Text(" "), Token::Time]);
let text = "[_time]";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(config.tokens[0], vec![Token::Time]);
let text = "[_time] ";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(config.tokens[0], vec![Token::Time, Token::Text(" ")]);
let text = "";
let mut config = ConfigBuilder::new().set_format_text(text, None).build();
config.calculate_data();
assert_eq!(config.tokens[0], vec![]);
}
}