use tracing_subscriber::{EnvFilter, fmt};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LogFormat {
#[default]
Pretty,
Json,
}
impl std::str::FromStr for LogFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"pretty" | "text" | "human" => Ok(LogFormat::Pretty),
"json" => Ok(LogFormat::Json),
_ => Err(format!("Invalid log format: {}. Use 'pretty' or 'json'", s)),
}
}
}
impl std::fmt::Display for LogFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
LogFormat::Pretty => write!(f, "pretty"),
LogFormat::Json => write!(f, "json"),
}
}
}
#[derive(Debug, Clone)]
pub struct LogConfig {
pub verbosity: u8,
pub format: LogFormat,
}
impl Default for LogConfig {
fn default() -> Self {
Self {
verbosity: 0,
format: LogFormat::Pretty,
}
}
}
impl LogConfig {
pub fn new(verbosity: u8) -> Self {
Self {
verbosity,
format: LogFormat::Pretty,
}
}
pub fn format(mut self, format: LogFormat) -> Self {
self.format = format;
self
}
fn filter(&self) -> EnvFilter {
let level = match self.verbosity {
0 => "warn",
1 => "info",
2 => "debug",
_ => "trace",
};
EnvFilter::new(level)
}
pub fn init(self) {
let filter = self.filter();
match self.format {
LogFormat::Pretty => {
fmt()
.with_env_filter(filter)
.with_target(false)
.without_time()
.init();
}
LogFormat::Json => {
fmt()
.with_env_filter(filter)
.json()
.with_current_span(true)
.init();
}
}
}
}
#[macro_export]
macro_rules! log_command {
($cmd:expr) => {
tracing::info!(command = $cmd, "Executing command");
};
($cmd:expr, $($field:tt)*) => {
tracing::info!(command = $cmd, $($field)*, "Executing command");
};
}
#[macro_export]
macro_rules! log_command_complete {
($cmd:expr, $duration_ms:expr) => {
tracing::info!(command = $cmd, duration_ms = $duration_ms, "Command completed");
};
($cmd:expr, $duration_ms:expr, $($field:tt)*) => {
tracing::info!(command = $cmd, duration_ms = $duration_ms, $($field)*, "Command completed");
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log_format_from_str() {
assert_eq!("pretty".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
assert_eq!("human".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
assert_eq!("JSON".parse::<LogFormat>().unwrap(), LogFormat::Json);
assert!("invalid".parse::<LogFormat>().is_err());
}
#[test]
fn log_format_display() {
assert_eq!(LogFormat::Pretty.to_string(), "pretty");
assert_eq!(LogFormat::Json.to_string(), "json");
}
#[test]
fn log_config_builder() {
let config = LogConfig::new(2).format(LogFormat::Json);
assert_eq!(config.verbosity, 2);
assert_eq!(config.format, LogFormat::Json);
}
#[test]
fn log_config_default() {
let config = LogConfig::default();
assert_eq!(config.verbosity, 0);
assert_eq!(config.format, LogFormat::Pretty);
}
#[test]
fn log_format_default_is_pretty() {
assert_eq!(LogFormat::default(), LogFormat::Pretty);
}
#[test]
fn log_format_from_str_error_message() {
let result = "invalid".parse::<LogFormat>();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("invalid"));
assert!(err.contains("pretty"));
assert!(err.contains("json"));
}
#[test]
fn log_config_new_sets_verbosity() {
assert_eq!(LogConfig::new(0).verbosity, 0);
assert_eq!(LogConfig::new(1).verbosity, 1);
assert_eq!(LogConfig::new(3).verbosity, 3);
}
#[test]
fn log_config_format_method_is_chainable() {
let config = LogConfig::new(1).format(LogFormat::Json);
assert_eq!(config.format, LogFormat::Json);
}
#[test]
fn log_config_clone() {
let config = LogConfig::new(2).format(LogFormat::Json);
let cloned = config.clone();
assert_eq!(cloned.verbosity, 2);
assert_eq!(cloned.format, LogFormat::Json);
}
#[test]
fn log_format_copy() {
let format = LogFormat::Json;
let copied = format;
assert_eq!(copied, LogFormat::Json);
}
#[test]
fn log_format_eq() {
assert_eq!(LogFormat::Pretty, LogFormat::Pretty);
assert_eq!(LogFormat::Json, LogFormat::Json);
assert_ne!(LogFormat::Pretty, LogFormat::Json);
}
#[test]
fn log_format_debug() {
assert!(!format!("{:?}", LogFormat::Pretty).is_empty());
assert!(!format!("{:?}", LogFormat::Json).is_empty());
}
#[test]
fn log_config_debug() {
let config = LogConfig::default();
let debug = format!("{:?}", config);
assert!(debug.contains("LogConfig"));
}
}