#![cfg_attr(docsrs, feature(doc_cfg))]
#![deny(missing_docs)]
pub extern crate colored;
pub extern crate jiff;
use std::fmt::Write;
use colored::Color;
use colored::ColoredString;
use colored::Colorize;
use jiff::Timestamp;
use jiff::tz::TimeZone;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::kv::KeyView;
use logforth_core::kv::ValueView;
use logforth_core::kv::Visitor;
use logforth_core::layout::Layout;
use logforth_core::record::Level;
use logforth_core::record::Record;
#[derive(Debug, Clone)]
pub struct TextLayout {
colors: LevelColor,
no_color: bool,
timezone: TimeZone,
timestamp_format: Option<fn(Timestamp, &TimeZone) -> String>,
}
impl Default for TextLayout {
fn default() -> Self {
Self {
colors: LevelColor::default(),
no_color: false,
timezone: TimeZone::system(),
timestamp_format: None,
}
}
}
impl TextLayout {
pub fn fatal_color(mut self, color: Color) -> Self {
self.colors.fatal = color;
self
}
pub fn error_color(mut self, color: Color) -> Self {
self.colors.error = color;
self
}
pub fn warn_color(mut self, color: Color) -> Self {
self.colors.warn = color;
self
}
pub fn info_color(mut self, color: Color) -> Self {
self.colors.info = color;
self
}
pub fn debug_color(mut self, color: Color) -> Self {
self.colors.debug = color;
self
}
pub fn trace_color(mut self, color: Color) -> Self {
self.colors.trace = color;
self
}
pub fn no_color(mut self) -> Self {
self.no_color = true;
self
}
pub fn timezone(mut self, tz: TimeZone) -> Self {
self.timezone = tz;
self
}
pub fn timestamp_format(mut self, format: fn(Timestamp, &TimeZone) -> String) -> Self {
self.timestamp_format = Some(format);
self
}
fn format_record_level(&self, level: Level) -> ColoredString {
self.colors.colorize_record_level(self.no_color, level)
}
}
struct KvWriter {
text: String,
}
impl Visitor for KvWriter {
fn visit(&mut self, key: KeyView, value: ValueView) -> Result<(), Error> {
use std::fmt::Write;
write!(&mut self.text, " {key}={value}").unwrap();
Ok(())
}
}
fn default_timestamp_format(ts: Timestamp, tz: &TimeZone) -> String {
let offset = tz.to_offset(ts);
format!("{:.6}", ts.display_with_offset(offset))
}
impl Layout for TextLayout {
fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
let ts = Timestamp::try_from(record.time()).unwrap();
let time = if let Some(format) = self.timestamp_format {
format(ts, &self.timezone)
} else {
default_timestamp_format(ts, &self.timezone)
};
let level = self.format_record_level(record.level());
let target = record.target();
let file = record.filename();
let line = record.line().unwrap_or_default();
let message = record.payload();
let mut visitor = KvWriter { text: time };
write!(
&mut visitor.text,
" {level:>6} {target}: {file}:{line} {message}"
)
.unwrap();
record.key_values().visit(&mut visitor)?;
for d in diags {
d.visit(&mut visitor)?;
}
Ok(visitor.text.into_bytes())
}
}
#[derive(Debug, Clone)]
struct LevelColor {
fatal: Color,
error: Color,
warn: Color,
info: Color,
debug: Color,
trace: Color,
}
impl Default for LevelColor {
fn default() -> Self {
Self {
fatal: Color::BrightRed,
error: Color::Red,
warn: Color::Yellow,
info: Color::Green,
debug: Color::Blue,
trace: Color::Magenta,
}
}
}
impl LevelColor {
fn colorize_record_level(&self, no_color: bool, level: Level) -> ColoredString {
if no_color {
ColoredString::from(level.to_string())
} else {
let color = match level {
Level::Fatal | Level::Fatal2 | Level::Fatal3 | Level::Fatal4 => self.fatal,
Level::Error | Level::Error2 | Level::Error3 | Level::Error4 => self.error,
Level::Warn | Level::Warn2 | Level::Warn3 | Level::Warn4 => self.warn,
Level::Info | Level::Info2 | Level::Info3 | Level::Info4 => self.info,
Level::Debug | Level::Debug2 | Level::Debug3 | Level::Debug4 => self.debug,
Level::Trace | Level::Trace2 | Level::Trace3 | Level::Trace4 => self.trace,
};
ColoredString::from(level.to_string()).color(color)
}
}
}