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 {
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::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));
}
pub trait HttpLogLike: serde::Serialize {
fn timestamp(&self) -> &str;
fn method(&self) -> &str;
fn path(&self) -> &str;
fn http_status(&self) -> i64;
fn total_duration(&self) -> i64;
fn request_id(&self) -> &str;
}
pub fn format_http_log_string<T: HttpLogLike>(log: &T, json: bool) -> String {
if json {
serde_json::to_string(log).unwrap()
} else {
let status = log.http_status();
let status = match status {
200..=299 => status.to_string().green(),
300..=399 => status.to_string().cyan(),
400..=499 => status.to_string().yellow(),
500..=599 => status.to_string().red(),
_ => status.to_string().normal(),
};
format!(
"{} {} {} {} {} {}",
log.timestamp().dimmed(),
log.method().bold(),
log.path(),
status.bold(),
format!("{}ms", log.total_duration()).dimmed(),
log.request_id().dimmed()
)
}
}
pub fn print_http_log<T: HttpLogLike>(log: T, json: bool) {
println!("{}", format_http_log_string(&log, json));
}
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()
}
}
impl HttpLogLike for queries::http_logs::HttpLogFields {
fn timestamp(&self) -> &str {
&self.timestamp
}
fn method(&self) -> &str {
&self.method
}
fn path(&self) -> &str {
&self.path
}
fn http_status(&self) -> i64 {
self.http_status
}
fn total_duration(&self) -> i64 {
self.total_duration
}
fn request_id(&self) -> &str {
&self.request_id
}
}
impl HttpLogLike for subscriptions::http_logs::HttpLogFields {
fn timestamp(&self) -> &str {
&self.timestamp
}
fn method(&self) -> &str {
&self.method
}
fn path(&self) -> &str {
&self.path
}
fn http_status(&self) -> i64 {
self.http_status
}
fn total_duration(&self) -> i64 {
self.total_duration
}
fn request_id(&self) -> &str {
&self.request_id
}
}
#[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::Full);
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); }
}