use crate::{colors::ColorSettings, logger::LogObject};
use is_terminal::IsTerminal;
use serde::ser::{SerializeMap, Serializer as _};
use serde_json::{
Serializer, Value,
ser::{CompactFormatter, Formatter, PrettyFormatter},
};
use std::io::{self, Write, stderr};
pub const RESERVED_FIELD_NAMES: [&str; 4] = ["level", "context", "message", "data"];
pub struct FormatState {
pub timestamp_format: String,
pub timestamp_key: String,
pub color_settings: ColorSettings,
pub pretty: bool,
pub context_fields: Vec<(String, Value)>,
}
pub fn write_log_line<W: Write>(
mut writer: W,
log: &LogObject,
state: &FormatState,
) -> io::Result<()> {
if stderr().is_terminal() {
let colored = format_with_colors(log, state);
writer.write_all(colored.as_bytes())?;
writer.write_all(b"\n")
} else {
if state.pretty {
let mut serializer =
Serializer::with_formatter(&mut writer, PrettyFormatter::with_indent(b" "));
write_one_log(&mut serializer, log, state)?;
} else {
let mut serializer = Serializer::with_formatter(&mut writer, CompactFormatter);
write_one_log(&mut serializer, log, state)?;
}
writer.write_all(b"\n")
}
}
fn write_one_log<W, F>(
serializer: &mut serde_json::Serializer<W, F>,
log: &LogObject,
state: &FormatState,
) -> Result<(), serde_json::Error>
where
W: Write,
F: Formatter,
{
let mut obj = serializer.serialize_map(None)?;
obj.serialize_entry("level", log.log_level.as_str())?;
let timestamp = log.timestamp.format(&state.timestamp_format).to_string();
obj.serialize_entry(&state.timestamp_key, ×tamp)?;
if let Some(msg) = &log.message {
obj.serialize_entry("message", msg)?;
}
if log.message.is_none() && log.data.as_str().is_some() {
obj.serialize_entry("message", log.data.as_str().unwrap())?;
} else {
obj.serialize_entry("data", &log.data)?;
}
for (k, v) in &state.context_fields {
obj.serialize_entry(k, v)?;
}
obj.end()
}
fn format_with_colors(log: &LogObject, state: &FormatState) -> String {
let level_plain = log.log_level.as_str();
let level_colored = log.log_level.get_colored_string(&state.color_settings);
if state.pretty {
let mut output = serde_json::Map::new();
output.insert("level".to_string(), Value::String(level_plain.to_string()));
output.insert(
state.timestamp_key.clone(),
Value::String(log.timestamp.format(&state.timestamp_format).to_string()),
);
if let Some(msg) = &log.message {
output.insert("message".to_string(), Value::String(msg.clone()));
}
if log.message.is_none() && log.data.as_str().is_some() {
output.insert("message".to_string(), log.data.clone());
} else {
output.insert("data".to_string(), log.data.clone());
}
for (k, v) in &state.context_fields {
output.insert(k.clone(), v.clone());
}
let json_output = Value::Object(output);
let pretty_json = serde_json::to_string_pretty(&json_output).unwrap();
pretty_json.replace(
&format!(r#""level": "{}""#, level_plain),
&format!(r#""level": "{}""#, level_colored),
)
} else {
let context_fields = state
.context_fields
.iter()
.map(|(k, v)| format!(r#""{}": {}"#, k, serde_json::to_string(v).unwrap()))
.collect::<Vec<_>>()
.join(", ");
let context_part = if context_fields.is_empty() {
String::new()
} else {
format!(",{context_fields}")
};
log.message.as_ref().map_or_else(
|| {
if log.data.as_str().is_some() {
format!(
r#"{{"level":"{}","{}":"{}","message":"{}"{}}}"#,
level_colored,
state.timestamp_key,
log.timestamp.format(&state.timestamp_format),
log.data.as_str().unwrap(),
context_part
)
} else {
format!(
r#"{{"level":"{}","{}":"{}","data":{}{}}}"#,
level_colored,
state.timestamp_key,
log.timestamp.format(&state.timestamp_format),
serde_json::to_string(&log.data).unwrap(),
context_part
)
}
},
|msg| {
format!(
r#"{{"level":"{}","{}":"{}","message":"{}","data":{}{}}}"#,
level_colored,
state.timestamp_key,
log.timestamp.format(&state.timestamp_format),
msg,
serde_json::to_string(&log.data).unwrap(),
context_part
)
},
)
}
}