use ntime;
use std::io;
use crate::attributes;
use crate::attributes::{KEY_ERROR, KEY_MESSAGE, KEY_TIME, KEY_TIMESTAMP};
use crate::console::Color;
use crate::level::Level;
use crate::sink::LogUpdate;
#[derive(Clone, Debug)]
pub enum OutputFormat {
Compact,
ColorCompact,
Json,
}
impl OutputFormat {
pub fn name(&self) -> String {
match self {
Self::Compact => "compact",
Self::ColorCompact => "compact (w/console color)",
Self::Json => "JSON",
}
.into()
}
}
pub struct FormatterConfig {
pub format: OutputFormat,
pub time_format: ntime::Format,
}
impl FormatterConfig {
pub fn default() -> Self {
Self {
format: OutputFormat::Compact,
time_format: ntime::Format::LocalMillisDateTime,
}
}
pub fn color() -> Self {
Self {
format: OutputFormat::ColorCompact,
time_format: ntime::Format::LocalMillisDateTime,
}
}
pub fn json() -> Self {
Self {
format: OutputFormat::Json,
time_format: ntime::Format::TimestampMilliseconds,
}
}
}
pub struct Formatter {
format: OutputFormat,
time_key: String,
time_format: ntime::Format,
}
impl Formatter {
pub fn new(conf: FormatterConfig) -> Self {
Self {
format: conf.format,
time_key: match &conf.time_format {
ntime::Format::TimestampSeconds | ntime::Format::TimestampMilliseconds => String::from(KEY_TIMESTAMP),
_ => String::from(KEY_TIME),
},
time_format: conf.time_format,
}
}
fn format_compact<T: io::Write>(&self, out: &mut T, update: &LogUpdate, attrs: &attributes::Map) -> io::Result<()> {
update.when.write(out, &self.time_format)?;
write!(out, " [{level}] {msg}", level = update.level.as_short_str(), msg = update.msg)?;
for (key, val) in attrs.into_iter() {
write!(out, " {key}=")?;
val.write_quoted(out)?;
}
Ok(())
}
fn format_color_compact<T: io::Write>(&self, out: &mut T, update: &LogUpdate, attrs: &attributes::Map) -> io::Result<()> {
let msg_color = if Level::Debug.includes(&update.level) { Color::Default } else { Color::BrightWhite };
let level_color = update.level.color();
update.when.write(out, &self.time_format)?;
write!(
out,
" {level_open}{level}{level_close} {msg_open}{msg}{msg_close}",
level_open = level_color.to_escape_str(),
level = update.level.as_short_str(),
level_close = Color::Default.to_escape_str(),
msg_open = msg_color.to_escape_str(),
msg = update.msg,
msg_close = Color::Default.to_escape_str(),
)?;
for (key, val) in attrs.into_iter() {
write!(
out,
" {key_open}{key}{key_close}={val_open}",
key_open = Color::Cyan.to_escape_str(),
key_close = Color::Default.to_escape_str(),
val_open = if key == KEY_ERROR { Color::BrightRed.to_escape_str() } else { "" }
)?;
val.write_quoted(out)?;
write!(out, "{val_close}", val_close = Color::Default.to_escape_str())?;
}
Ok(())
}
fn format_json<T: io::Write>(&self, out: &mut T, update: &LogUpdate, attrs: &attributes::Map) -> io::Result<()> {
match self.time_format.as_integer(&update.when) {
Some(timestamp_int) => write!(
out,
"{{\"{time_key}\":{timestamp_int},\"level\":\"{level}\",\"{msg_key}\":\"{msg}\"",
time_key = self.time_key,
level = update.level.as_str(),
msg_key = KEY_MESSAGE,
msg = update.msg,
)?,
None => {
write!(out, "{{\"{time_key}\":\"", time_key = self.time_key)?;
update.when.write(out, &self.time_format)?;
write!(
out,
"\",\"level\":\"{level}\",\"{msg_key}\":\"{msg}\"",
level = update.level.as_str(),
msg_key = KEY_MESSAGE,
msg = update.msg,
)?;
}
}
for (key, val) in attrs.into_iter() {
write!(out, ",\"{key}\":")?;
val.write_json(out)?;
}
write!(out, "}}")?;
Ok(())
}
pub fn write<T: io::Write>(&self, out: &mut T, update: &LogUpdate, attrs: &attributes::Map) -> io::Result<()> {
match self.format {
OutputFormat::Compact => self.format_compact(out, update, attrs),
OutputFormat::ColorCompact => self.format_color_compact(out, update, attrs),
OutputFormat::Json => self.format_json(out, &update, attrs),
}
}
pub fn as_string(&self, update: &LogUpdate, attrs: &attributes::Map) -> String {
let mut out = Vec::new();
match self.write(&mut out, update, attrs) {
Ok(_) => (),
Err(e) => panic!("failed to convert log update {update:?} to string buffer: {e}"),
};
match String::from_utf8(out) {
Ok(s) => s,
Err(e) => panic!("failed to convert log update {update:?} to UTF8: {e}"),
}
}
}
pub fn as_panic_string(update: &LogUpdate, attrs: &attributes::Map) -> String {
let formatter = Formatter::new(FormatterConfig {
format: OutputFormat::Compact,
..FormatterConfig::default()
});
formatter.as_string(update, attrs)
}