use regex::Regex;
use serde_json::Value;
use std::sync::LazyLock;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LogLevel {
Info,
Debug,
Trace,
}
impl LogLevel {
pub fn prefix(&self) -> &'static str {
match self {
LogLevel::Info => "[INFO]",
LogLevel::Debug => "[DEBUG]",
LogLevel::Trace => "[TRACE]",
}
}
}
#[derive(Clone, Debug, Default)]
pub struct LogTemplate {
pub info: Option<String>,
pub debug: Option<String>,
pub trace: Option<String>,
}
impl LogTemplate {
pub fn new() -> Self {
Self::default()
}
pub fn with_info(mut self, template: impl Into<String>) -> Self {
self.info = Some(template.into());
self
}
pub fn with_debug(mut self, template: impl Into<String>) -> Self {
self.debug = Some(template.into());
self
}
pub fn with_trace(mut self, template: impl Into<String>) -> Self {
self.trace = Some(template.into());
self
}
pub fn get_template(&self, level: LogLevel) -> Option<&str> {
match level {
LogLevel::Info => self.info.as_deref(),
LogLevel::Debug => self.debug.as_deref(),
LogLevel::Trace => self.trace.as_deref(),
}
}
pub fn render(&self, level: LogLevel, data: &Value) -> Option<String> {
self.get_template(level).map(|t| render_template(t, data))
}
pub fn has_any(&self) -> bool {
self.info.is_some() || self.debug.is_some() || self.trace.is_some()
}
}
static PLACEHOLDER_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"\{([^}]+)\}").expect("Invalid regex"));
fn render_template(template: &str, data: &Value) -> String {
PLACEHOLDER_REGEX
.replace_all(template, |cap: ®ex::Captures<'_>| {
let placeholder = &cap[1];
crate::utils::sanitize::line_field(&render_placeholder(placeholder, data))
})
.into_owned()
}
fn render_placeholder(placeholder: &str, data: &Value) -> String {
if placeholder == "." {
return serde_json::to_string(data).unwrap_or_else(|_| "{}".to_string());
}
if placeholder.starts_with("json_pretty(") && placeholder.ends_with(')') {
let field = &placeholder[12..placeholder.len() - 1];
let value = if field == "." {
data.clone()
} else {
get_nested_value(data, field)
};
return serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string());
}
if placeholder.starts_with("json(") && placeholder.ends_with(')') {
let field = &placeholder[5..placeholder.len() - 1];
let value = if field == "." {
data.clone()
} else {
get_nested_value(data, field)
};
return serde_json::to_string(&value).unwrap_or_else(|_| "{}".to_string());
}
if placeholder.starts_with("hex(") && placeholder.ends_with(')') {
let field = &placeholder[4..placeholder.len() - 1];
let value = get_nested_value(data, field);
if let Some(s) = value.as_str() {
return hex::encode(s.as_bytes());
}
return String::new();
}
if placeholder.starts_with("preview(") && placeholder.ends_with(')') {
let inner = &placeholder[8..placeholder.len() - 1];
let (field, max_len) = if let Some(comma_pos) = inner.find(',') {
let field = inner[..comma_pos].trim();
let len_str = inner[comma_pos + 1..].trim();
let len = len_str.parse::<usize>().unwrap_or(100);
(field, len)
} else {
(inner, 100)
};
let value = get_nested_value(data, field);
let s = value_to_string(&value);
return crate::utils::truncate_for_log(&s, max_len);
}
if placeholder.ends_with("_len") {
let field = &placeholder[..placeholder.len() - 4];
let value = get_nested_value(data, field);
return match &value {
Value::String(s) => s.len().to_string(),
Value::Array(a) => a.len().to_string(),
Value::Object(o) => o.len().to_string(),
_ => "0".to_string(),
};
}
let value = get_nested_value(data, placeholder);
value_to_string(&value)
}
fn get_nested_value(data: &Value, path: &str) -> Value {
let parts: Vec<&str> = path.split('.').collect();
let mut current = data.clone();
for part in parts {
if let Some(bracket_pos) = part.find('[') {
let field = &part[..bracket_pos];
let index_str = &part[bracket_pos + 1..part.len() - 1];
if let Ok(index) = index_str.parse::<usize>() {
current = current
.get(field)
.and_then(|v| v.get(index))
.cloned()
.unwrap_or(Value::Null);
continue;
}
}
current = current.get(part).cloned().unwrap_or(Value::Null);
}
current
}
fn value_to_string(value: &Value) -> String {
match value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => String::new(),
Value::Array(_) | Value::Object(_) => {
serde_json::to_string(value).unwrap_or_else(|_| String::new())
}
}
}