1use tracing_subscriber::{EnvFilter, fmt};
6
7#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
9pub enum LogFormat {
10 #[default]
12 Pretty,
13 Json,
15}
16
17impl std::str::FromStr for LogFormat {
18 type Err = String;
19
20 fn from_str(s: &str) -> Result<Self, Self::Err> {
21 match s.to_lowercase().as_str() {
22 "pretty" | "text" | "human" => Ok(LogFormat::Pretty),
23 "json" => Ok(LogFormat::Json),
24 _ => Err(format!("Invalid log format: {}. Use 'pretty' or 'json'", s)),
25 }
26 }
27}
28
29impl std::fmt::Display for LogFormat {
30 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31 match self {
32 LogFormat::Pretty => write!(f, "pretty"),
33 LogFormat::Json => write!(f, "json"),
34 }
35 }
36}
37
38#[derive(Debug, Clone)]
40pub struct LogConfig {
41 pub verbosity: u8,
43 pub format: LogFormat,
45}
46
47impl Default for LogConfig {
48 fn default() -> Self {
49 Self {
50 verbosity: 0,
51 format: LogFormat::Pretty,
52 }
53 }
54}
55
56impl LogConfig {
57 pub fn new(verbosity: u8) -> Self {
59 Self {
60 verbosity,
61 format: LogFormat::Pretty,
62 }
63 }
64
65 pub fn format(mut self, format: LogFormat) -> Self {
67 self.format = format;
68 self
69 }
70
71 fn filter(&self) -> EnvFilter {
73 let level = match self.verbosity {
74 0 => "warn",
75 1 => "info",
76 2 => "debug",
77 _ => "trace",
78 };
79 EnvFilter::new(level)
80 }
81
82 pub fn init(self) {
86 let filter = self.filter();
87
88 match self.format {
89 LogFormat::Pretty => {
90 fmt()
91 .with_env_filter(filter)
92 .with_target(false)
93 .without_time()
94 .init();
95 }
96 LogFormat::Json => {
97 fmt()
98 .with_env_filter(filter)
99 .json()
100 .with_current_span(true)
101 .init();
102 }
103 }
104 }
105}
106
107#[macro_export]
111macro_rules! log_command {
112 ($cmd:expr) => {
113 tracing::info!(command = $cmd, "Executing command");
114 };
115 ($cmd:expr, $($field:tt)*) => {
116 tracing::info!(command = $cmd, $($field)*, "Executing command");
117 };
118}
119
120#[macro_export]
122macro_rules! log_command_complete {
123 ($cmd:expr, $duration_ms:expr) => {
124 tracing::info!(command = $cmd, duration_ms = $duration_ms, "Command completed");
125 };
126 ($cmd:expr, $duration_ms:expr, $($field:tt)*) => {
127 tracing::info!(command = $cmd, duration_ms = $duration_ms, $($field)*, "Command completed");
128 };
129}
130
131#[cfg(test)]
132mod tests {
133 use super::*;
134
135 #[test]
136 fn log_format_from_str() {
137 assert_eq!("pretty".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
138 assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
139 assert_eq!("human".parse::<LogFormat>().unwrap(), LogFormat::Pretty);
140 assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
141 assert_eq!("JSON".parse::<LogFormat>().unwrap(), LogFormat::Json);
142 assert!("invalid".parse::<LogFormat>().is_err());
143 }
144
145 #[test]
146 fn log_format_display() {
147 assert_eq!(LogFormat::Pretty.to_string(), "pretty");
148 assert_eq!(LogFormat::Json.to_string(), "json");
149 }
150
151 #[test]
152 fn log_config_builder() {
153 let config = LogConfig::new(2).format(LogFormat::Json);
154 assert_eq!(config.verbosity, 2);
155 assert_eq!(config.format, LogFormat::Json);
156 }
157
158 #[test]
159 fn log_config_default() {
160 let config = LogConfig::default();
161 assert_eq!(config.verbosity, 0);
162 assert_eq!(config.format, LogFormat::Pretty);
163 }
164
165 #[test]
166 fn log_format_default_is_pretty() {
167 assert_eq!(LogFormat::default(), LogFormat::Pretty);
168 }
169
170 #[test]
171 fn log_format_from_str_error_message() {
172 let result = "invalid".parse::<LogFormat>();
173 assert!(result.is_err());
174 let err = result.unwrap_err();
175 assert!(err.contains("invalid"));
176 assert!(err.contains("pretty"));
177 assert!(err.contains("json"));
178 }
179
180 #[test]
181 fn log_config_new_sets_verbosity() {
182 assert_eq!(LogConfig::new(0).verbosity, 0);
183 assert_eq!(LogConfig::new(1).verbosity, 1);
184 assert_eq!(LogConfig::new(3).verbosity, 3);
185 }
186
187 #[test]
188 fn log_config_format_method_is_chainable() {
189 let config = LogConfig::new(1).format(LogFormat::Json);
190 assert_eq!(config.format, LogFormat::Json);
191 }
192
193 #[test]
194 fn log_config_clone() {
195 let config = LogConfig::new(2).format(LogFormat::Json);
196 let cloned = config.clone();
197 assert_eq!(cloned.verbosity, 2);
198 assert_eq!(cloned.format, LogFormat::Json);
199 }
200
201 #[test]
202 fn log_format_copy() {
203 let format = LogFormat::Json;
204 let copied = format;
205 assert_eq!(copied, LogFormat::Json);
206 }
207
208 #[test]
209 fn log_format_eq() {
210 assert_eq!(LogFormat::Pretty, LogFormat::Pretty);
211 assert_eq!(LogFormat::Json, LogFormat::Json);
212 assert_ne!(LogFormat::Pretty, LogFormat::Json);
213 }
214
215 #[test]
216 fn log_format_debug() {
217 assert!(!format!("{:?}", LogFormat::Pretty).is_empty());
218 assert!(!format!("{:?}", LogFormat::Json).is_empty());
219 }
220
221 #[test]
222 fn log_config_debug() {
223 let config = LogConfig::default();
224 let debug = format!("{:?}", config);
225 assert!(debug.contains("LogConfig"));
226 }
227}