use super::LogEvent;
use crate::config::LoggingConfig;
use std::collections::BTreeMap;
pub(super) const ANSI_RESET: &str = "\x1b[0m";
pub(super) const ANSI_DIM: &str = "\x1b[2m";
pub(super) const ANSI_BOLD: &str = "\x1b[1m";
pub(super) const ANSI_RED: &str = "\x1b[31m";
pub(super) const ANSI_GREEN: &str = "\x1b[32m";
pub(super) const ANSI_YELLOW: &str = "\x1b[33m";
pub(super) const ANSI_BLUE: &str = "\x1b[34m";
pub(super) const ANSI_CYAN: &str = "\x1b[36m";
pub(super) const ANSI_WHITE: &str = "\x1b[37m";
pub(super) const ANSI_BOLD_RED: &str = "\x1b[31;1m";
pub(super) const LEVEL_PAD: usize = 9;
pub(super) fn level_color(level_lower: &str) -> &'static str {
match level_lower {
"critical" | "fatal" => ANSI_BOLD_RED,
"error" => ANSI_RED,
"warning" | "warn" => ANSI_YELLOW,
"info" => ANSI_GREEN,
"debug" => ANSI_BLUE,
"trace" => ANSI_CYAN,
_ => "",
}
}
pub(super) fn resolve_named_color(name: &str) -> &'static str {
match name.trim().to_ascii_lowercase().as_str() {
"dim" => ANSI_DIM,
"bold" => ANSI_BOLD,
"red" => ANSI_RED,
"green" => ANSI_GREEN,
"yellow" => ANSI_YELLOW,
"blue" => ANSI_BLUE,
"cyan" => ANSI_CYAN,
"white" => ANSI_WHITE,
_ => "",
}
}
fn wrap(s: &str, color: &str, colors: bool) -> String {
if colors && !color.is_empty() {
format!("{color}{s}{ANSI_RESET}")
} else {
s.to_string()
}
}
fn format_level(level: &str, colors: bool) -> String {
let lower = level.to_ascii_lowercase();
let pad_width = LEVEL_PAD.saturating_sub(lower.chars().count());
let padded = format!("{lower}{}", " ".repeat(pad_width));
if colors {
let color = level_color(&lower);
if !color.is_empty() {
return format!("[{color}{padded}{ANSI_RESET}]");
}
}
format!("[{padded}]")
}
fn format_value(v: &serde_json::Value) -> String {
match v {
serde_json::Value::String(s) => format!("{s:?}"),
other => other.to_string(),
}
}
fn pretty_env_value(name: &str, default: &str) -> String {
std::env::var(name).unwrap_or_else(|_| default.to_string())
}
fn pretty_fields_filter() -> Option<Vec<String>> {
let raw = std::env::var("PROVIDE_LOG_PRETTY_FIELDS").ok()?;
let fields = raw
.split(',')
.map(str::trim)
.filter(|field| !field.is_empty())
.map(str::to_string)
.collect::<Vec<_>>();
if fields.is_empty() {
None
} else {
Some(fields)
}
}
fn field_allowed(fields: Option<&[String]>, key: &str) -> bool {
match fields {
Some(allowed) => allowed.iter().any(|field| field == key),
None => true,
}
}
pub(super) fn format_pretty_line_with_colors(
event: &LogEvent,
cfg: &LoggingConfig,
colors: bool,
) -> String {
let mut parts: Vec<String> = Vec::new();
if cfg.include_timestamp {
let ts = super::emit::now_iso8601();
parts.push(wrap(&ts, ANSI_DIM, colors));
}
parts.push(format_level(&event.level, colors));
parts.push(event.message.clone());
let key_color = resolve_named_color(&pretty_env_value("PROVIDE_LOG_PRETTY_KEY_COLOR", "dim"));
let value_color = resolve_named_color(&pretty_env_value("PROVIDE_LOG_PRETTY_VALUE_COLOR", ""));
let fields = pretty_fields_filter();
let mut display_fields = BTreeMap::new();
if !event.target.is_empty() {
display_fields.insert(
"logger_name".to_string(),
serde_json::Value::String(event.target.clone()),
);
}
display_fields.extend(event.context.clone());
if let Some(trace_id) = &event.trace_id {
display_fields.insert(
"trace_id".to_string(),
serde_json::Value::String(trace_id.clone()),
);
}
if let Some(span_id) = &event.span_id {
display_fields.insert(
"span_id".to_string(),
serde_json::Value::String(span_id.clone()),
);
}
for (k, v) in &display_fields {
if !field_allowed(fields.as_deref(), k) {
continue;
}
let kp = wrap(k, key_color, colors);
let vs = format_value(v);
let vp = wrap(&vs, value_color, colors);
parts.push(format!("{kp}={vp}"));
}
parts.join(" ")
}
pub(super) fn format_pretty_line(event: &LogEvent, cfg: &LoggingConfig) -> String {
use std::io::IsTerminal;
format_pretty_line_with_colors(event, cfg, std::io::stderr().is_terminal())
}
#[cfg(test)]
#[path = "pretty_tests.rs"]
mod tests;