use crate::{queries, subscriptions};
use colored::Colorize;
use serde_json::Value;
use std::collections::HashMap;
pub trait LogLike {
fn message(&self) -> &str;
fn timestamp(&self) -> &str;
fn attributes(&self) -> Vec<(&str, &str)>;
}
pub fn format_attr_log_string<T: LogLike>(log: &T, show_all_attributes: bool) -> String {
let timestamp = log.timestamp();
let message = log.message();
let attributes = log.attributes();
if attributes.is_empty() || (attributes.len() == 1 && attributes[0].0 == "level") {
return message.to_string();
}
let mut level: Option<String> = None;
let mut others = Vec::new();
for (key, value) in attributes {
match key.to_lowercase().as_str() {
"level" | "lvl" | "severity" => level = Some(value.to_string()),
_ => {
if show_all_attributes {
others.push(format!(
"{}{}{}",
key.magenta(),
"=",
value
.normal()
.replace('"', "\"".dimmed().to_string().as_str())
));
}
}
}
}
if let Some(level) = level {
let level_str = match level.replace('"', "").to_lowercase().as_str() {
"info" => "[INFO]".blue(),
"error" | "err" => "[ERRO]".red(),
"warn" => "[WARN]".yellow(),
"debug" => "[DBUG]".dimmed(),
_ => format!("[{level}]").normal(),
}
.bold();
if others.is_empty() {
format!("{} {}", level_str, message)
} else {
format!(
"{} {} {} {}",
timestamp.replace('"', "").normal(),
level_str,
message,
others.join(" ")
)
}
} else {
message.to_string()
}
}
#[derive(Clone, Copy)]
pub enum LogFormat {
Simple,
LevelOnly,
Full,
}
pub fn format_log_string<T>(log: T, json: bool, format: LogFormat) -> String
where
T: LogLike + serde::Serialize,
{
if json {
let mut map: HashMap<String, Value> = HashMap::new();
map.insert(
"message".to_string(),
serde_json::to_value(log.message()).unwrap(),
);
map.insert(
"timestamp".to_string(),
serde_json::to_value(log.timestamp()).unwrap(),
);
for (key, value) in log.attributes() {
let parsed_value = match value.trim_matches('"').parse::<Value>() {
Ok(v) => v,
Err(_) => serde_json::to_value(value.trim_matches('"')).unwrap(),
};
map.insert(key.to_string(), parsed_value);
}
serde_json::to_string(&map).unwrap()
} else {
match format {
LogFormat::Simple => log.message().to_string(),
LogFormat::LevelOnly => format_attr_log_string(&log, false),
LogFormat::Full => format_attr_log_string(&log, true),
}
}
}
pub fn print_log<T>(log: T, json: bool, format: LogFormat)
where
T: LogLike + serde::Serialize,
{
println!("{}", format_log_string(log, json, format));
}
impl LogLike for subscriptions::deployment_logs::LogFields {
fn message(&self) -> &str {
&self.message
}
fn timestamp(&self) -> &str {
&self.timestamp
}
fn attributes(&self) -> Vec<(&str, &str)> {
self.attributes
.iter()
.map(|a| (a.key.as_str(), a.value.as_str()))
.collect()
}
}
impl LogLike for queries::deployment_logs::LogFields {
fn message(&self) -> &str {
&self.message
}
fn timestamp(&self) -> &str {
&self.timestamp
}
fn attributes(&self) -> Vec<(&str, &str)> {
self.attributes
.iter()
.map(|a| (a.key.as_str(), a.value.as_str()))
.collect()
}
}
impl LogLike for subscriptions::build_logs::LogFields {
fn message(&self) -> &str {
&self.message
}
fn timestamp(&self) -> &str {
&self.timestamp
}
fn attributes(&self) -> Vec<(&str, &str)> {
self.attributes
.iter()
.map(|a| (a.key.as_str(), a.value.as_str()))
.collect()
}
}
impl LogLike for queries::build_logs::LogFields {
fn message(&self) -> &str {
&self.message
}
fn timestamp(&self) -> &str {
&self.timestamp
}
fn attributes(&self) -> Vec<(&str, &str)> {
self.attributes
.iter()
.map(|a| (a.key.as_str(), a.value.as_str()))
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(serde::Serialize)]
struct TestLog {
message: String,
timestamp: String,
attributes: Vec<(String, String)>,
}
impl LogLike for TestLog {
fn message(&self) -> &str {
&self.message
}
fn timestamp(&self) -> &str {
&self.timestamp
}
fn attributes(&self) -> Vec<(&str, &str)> {
self.attributes
.iter()
.map(|(k, v)| (k.as_str(), v.as_str()))
.collect()
}
}
#[test]
fn test_format_attr_log_no_attributes() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![],
};
let output = format_attr_log_string(&log, false);
assert_eq!(output, "Test message");
}
#[test]
fn test_format_attr_log_only_level() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![("level".to_string(), "info".to_string())],
};
let output = format_attr_log_string(&log, false);
assert_eq!(output, "Test message");
}
#[test]
fn test_format_attr_log_with_attributes_level_only() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![
("level".to_string(), "error".to_string()),
("service".to_string(), "api".to_string()),
("replica".to_string(), "xyz123".to_string()),
],
};
let output = format_attr_log_string(&log, false);
assert!(output.contains("Test message"));
assert!(!output.contains("service"));
assert!(!output.contains("api"));
}
#[test]
fn test_format_attr_log_with_attributes_full() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![
("level".to_string(), "error".to_string()),
("service".to_string(), "api".to_string()),
("replica".to_string(), "xyz123".to_string()),
],
};
let output = format_attr_log_string(&log, true);
assert!(output.contains("Test message"));
assert!(output.contains("2025-01-01T00:00:00Z"));
assert!(output.contains("service"));
assert!(output.contains("api"));
assert!(output.contains("replica"));
assert!(output.contains("xyz123"));
}
#[test]
fn test_print_log_json_mode() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![
("level".to_string(), "warn".to_string()),
("count".to_string(), "42".to_string()),
],
};
let output = format_log_string(log, true, LogFormat::Simple);
let json: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(json["message"], "Test message");
assert_eq!(json["timestamp"], "2025-01-01T00:00:00Z");
assert_eq!(json["level"], "warn");
assert_eq!(json["count"], 42); }
#[test]
fn test_print_log_simple_mode() {
let log = TestLog {
message: "Test message".to_string(),
timestamp: "2025-01-01T00:00:00Z".to_string(),
attributes: vec![("level".to_string(), "info".to_string())],
};
let output = format_log_string(log, false, LogFormat::Simple);
assert_eq!(output, "Test message");
}
}