Skip to main content

netspeed_cli/
logging.rs

1//! Structured logging infrastructure for netspeed-cli.
2//!
3//! This module provides logging utilities that can be used throughout the application.
4//! It supports log levels, structured output, and runtime log level configuration.
5
6use std::env;
7
8/// Log level enumeration matching common severity levels.
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum Level {
11    /// Debug-level messages for development and troubleshooting
12    Debug,
13    /// Informational messages about normal operation
14    #[default]
15    Info,
16    /// Warning messages indicating potential issues
17    Warn,
18    /// Error messages indicating failures
19    Error,
20}
21
22impl Level {
23    /// Parse log level from environment variable value.
24    #[must_use]
25    pub fn from_env(var: &str) -> Self {
26        match var.to_lowercase().as_str() {
27            "debug" => Level::Debug,
28            "info" => Level::Info,
29            "warn" | "warning" => Level::Warn,
30            "error" | "err" => Level::Error,
31            _ => Level::Info,
32        }
33    }
34
35    /// Get the environment variable name for log level.
36    #[must_use]
37    pub const fn env_var() -> &'static str {
38        "NETSPEED_LOG"
39    }
40
41    /// Check if this level should be logged given the current threshold.
42    #[must_use]
43    pub fn should_log(self, threshold: Level) -> bool {
44        self as u8 >= threshold as u8
45    }
46}
47
48/// Get the current log level from the NETSPEED_LOG environment variable.
49#[must_use]
50pub fn current_level() -> Level {
51    env::var(Level::env_var())
52        .ok()
53        .map(|v| Level::from_env(&v))
54        .unwrap_or_default()
55}
56
57/// Check if verbose logging is enabled.
58#[must_use]
59pub fn is_verbose() -> bool {
60    current_level() == Level::Debug
61}
62
63/// Log a message with structured key-value pairs.
64pub fn log(level: Level, message: &str, fields: &[(&str, &str)]) {
65    if level.should_log(current_level()) {
66        eprint!("[{}] {}", format!("{:?}", level).to_uppercase(), message);
67        for (key, value) in fields {
68            eprint!(" {}=\"{}\"", key, value);
69        }
70        eprintln!();
71    }
72}
73
74/// Log a debug message.
75pub fn debug(message: &str) {
76    log(Level::Debug, message, &[]);
77}
78
79/// Log an info message.
80pub fn info(message: &str) {
81    log(Level::Info, message, &[]);
82}
83
84/// Log a warning message.
85pub fn warn(message: &str) {
86    log(Level::Warn, message, &[]);
87}
88
89/// Log an error message.
90pub fn error(message: &str) {
91    log(Level::Error, message, &[]);
92}
93
94/// Format a structured log entry as JSON for machine-readable output.
95#[must_use]
96pub fn format_json_entry(level: Level, message: &str, fields: &[(&str, &str)]) -> String {
97    use serde_json::json;
98    let mut map = serde_json::Map::new();
99    map.insert(
100        "level".to_string(),
101        json!(format!("{:?}", level).to_lowercase()),
102    );
103    map.insert("message".to_string(), json!(message));
104    map.insert(
105        "timestamp".to_string(),
106        json!(chrono::Utc::now().to_rfc3339()),
107    );
108    for (key, value) in fields {
109        map.insert(key.to_string(), json!(value));
110    }
111    serde_json::to_string(&map).unwrap_or_else(|_| "{\"error\": \"log format failed\"}".to_string())
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    // ── Level enum tests ────────────────────────────────────────────────────
119
120    #[test]
121    fn test_level_from_env_debug() {
122        assert_eq!(Level::from_env("debug"), Level::Debug);
123        assert_eq!(Level::from_env("DEBUG"), Level::Debug);
124        assert_eq!(Level::from_env("Debug"), Level::Debug);
125    }
126
127    #[test]
128    fn test_level_from_env_info() {
129        assert_eq!(Level::from_env("info"), Level::Info);
130        assert_eq!(Level::from_env("INFO"), Level::Info);
131        assert_eq!(Level::from_env("Info"), Level::Info);
132    }
133
134    #[test]
135    fn test_level_from_env_warn() {
136        assert_eq!(Level::from_env("warn"), Level::Warn);
137        assert_eq!(Level::from_env("warning"), Level::Warn);
138        assert_eq!(Level::from_env("WARN"), Level::Warn);
139        assert_eq!(Level::from_env("WARNING"), Level::Warn);
140        assert_eq!(Level::from_env("Warn"), Level::Warn);
141    }
142
143    #[test]
144    fn test_level_from_env_error() {
145        assert_eq!(Level::from_env("error"), Level::Error);
146        assert_eq!(Level::from_env("err"), Level::Error);
147        assert_eq!(Level::from_env("ERROR"), Level::Error);
148        assert_eq!(Level::from_env("Err"), Level::Error);
149    }
150
151    #[test]
152    fn test_level_from_env_invalid() {
153        assert_eq!(Level::from_env("invalid"), Level::Info);
154        assert_eq!(Level::from_env("trash"), Level::Info);
155        assert_eq!(Level::from_env(""), Level::Info);
156        assert_eq!(Level::from_env("123"), Level::Info);
157    }
158
159    #[test]
160    fn test_level_should_log() {
161        // Debug should_log tests
162        assert!(Level::Debug.should_log(Level::Debug));
163        assert!(Level::Info.should_log(Level::Debug)); // Info >= Debug threshold
164        assert!(Level::Warn.should_log(Level::Debug));
165        assert!(Level::Error.should_log(Level::Debug));
166
167        // Info should_log tests
168        assert!(!Level::Debug.should_log(Level::Info)); // Debug < Info threshold
169        assert!(Level::Info.should_log(Level::Info));
170        assert!(Level::Warn.should_log(Level::Info));
171        assert!(Level::Error.should_log(Level::Info));
172
173        // Warn should_log tests
174        assert!(!Level::Debug.should_log(Level::Warn));
175        assert!(!Level::Info.should_log(Level::Warn));
176        assert!(Level::Warn.should_log(Level::Warn));
177        assert!(Level::Error.should_log(Level::Warn));
178
179        // Error should_log tests
180        assert!(!Level::Debug.should_log(Level::Error));
181        assert!(!Level::Info.should_log(Level::Error));
182        assert!(!Level::Warn.should_log(Level::Error));
183        assert!(Level::Error.should_log(Level::Error));
184    }
185
186    #[test]
187    fn test_level_default() {
188        assert_eq!(Level::default(), Level::Info);
189    }
190
191    #[test]
192    fn test_level_debug_trait() {
193        let debug_str = format!("{:?}", Level::Debug);
194        assert!(debug_str.contains("Debug"));
195
196        let debug_str = format!("{:?}", Level::Info);
197        assert!(debug_str.contains("Info"));
198
199        let debug_str = format!("{:?}", Level::Warn);
200        assert!(debug_str.contains("Warn"));
201
202        let debug_str = format!("{:?}", Level::Error);
203        assert!(debug_str.contains("Error"));
204    }
205
206    #[test]
207    fn test_level_copy() {
208        let level = Level::Debug;
209        let _copied = level; // Copy is implicit for Copy types
210        assert_eq!(_copied, level);
211    }
212
213    #[test]
214    fn test_level_eq() {
215        assert_eq!(Level::Debug, Level::Debug);
216        assert_eq!(Level::Info, Level::Info);
217        assert_eq!(Level::Warn, Level::Warn);
218        assert_eq!(Level::Error, Level::Error);
219        assert_ne!(Level::Debug, Level::Info);
220        assert_ne!(Level::Info, Level::Error);
221    }
222
223    #[test]
224    fn test_level_partial_eq() {
225        assert!(Level::Debug == Level::Debug);
226        assert!(Level::Debug != Level::Error);
227    }
228
229    #[test]
230    fn test_level_env_var() {
231        assert_eq!(Level::env_var(), "NETSPEED_LOG");
232    }
233
234    // ── current_level tests ─────────────────────────────────────────────────
235
236    #[test]
237    fn test_current_level_returns_info_by_default() {
238        // Without NETSPEED_LOG set, should return Info
239        let level = current_level();
240        assert_eq!(level, Level::Info);
241    }
242
243    // ── is_verbose tests ────────────────────────────────────────────────────
244
245    #[test]
246    fn test_is_verbose_by_default() {
247        // Without NETSPEED_LOG set to debug, should not be verbose
248        let verbose = is_verbose();
249        assert!(!verbose);
250    }
251
252    // ── log function tests ──────────────────────────────────────────────────
253
254    #[test]
255    fn test_log_empty_fields() {
256        // Should not panic and should produce output
257        log(Level::Info, "test message", &[]);
258    }
259
260    #[test]
261    fn test_log_with_fields() {
262        log(
263            Level::Debug,
264            "test",
265            &[("key1", "value1"), ("key2", "value2")],
266        );
267    }
268
269    #[test]
270    fn test_log_special_characters_in_fields() {
271        log(Level::Info, "test", &[("key", "value with spaces")]);
272        log(Level::Info, "test", &[("key", "value\"with\"quotes")]);
273        log(Level::Info, "test", &[("key", "")]);
274    }
275
276    // ── shortcut logging functions tests ────────────────────────────────────
277
278    #[test]
279    fn test_debug_function() {
280        debug("debug message");
281    }
282
283    #[test]
284    fn test_info_function() {
285        info("info message");
286    }
287
288    #[test]
289    fn test_warn_function() {
290        warn("warning message");
291    }
292
293    #[test]
294    fn test_error_function() {
295        error("error message");
296    }
297
298    // ── format_json_entry tests ─────────────────────────────────────────────
299
300    #[test]
301    fn test_format_json_entry_debug() {
302        let entry = format_json_entry(Level::Debug, "debug message", &[("key", "value")]);
303        assert!(entry.contains("debug"));
304        assert!(entry.contains("debug message"));
305        assert!(entry.contains("timestamp"));
306        assert!(entry.contains("key"));
307        assert!(entry.contains("value"));
308    }
309
310    #[test]
311    fn test_format_json_entry_info() {
312        let entry = format_json_entry(Level::Info, "test message", &[("key", "value")]);
313        assert!(entry.contains("info"));
314        assert!(entry.contains("test message"));
315        assert!(entry.contains("timestamp"));
316        assert!(entry.contains("key"));
317        assert!(entry.contains("value"));
318    }
319
320    #[test]
321    fn test_format_json_entry_warn() {
322        let entry = format_json_entry(Level::Warn, "warning message", &[]);
323        assert!(entry.contains("warn"));
324        assert!(entry.contains("warning message"));
325    }
326
327    #[test]
328    fn test_format_json_entry_error() {
329        let entry = format_json_entry(Level::Error, "error occurred", &[]);
330        assert!(entry.contains("error"));
331        assert!(entry.contains("error occurred"));
332    }
333
334    #[test]
335    fn test_format_json_entry_empty_fields() {
336        let entry = format_json_entry(Level::Error, "error occurred", &[]);
337        assert!(entry.contains("error"));
338        assert!(entry.contains("error occurred"));
339        // Should still have timestamp even with no fields
340        assert!(entry.contains("timestamp"));
341    }
342
343    #[test]
344    fn test_format_json_entry_multiple_fields() {
345        let entry = format_json_entry(
346            Level::Info,
347            "multi-field message",
348            &[
349                ("field1", "value1"),
350                ("field2", "value2"),
351                ("field3", "value3"),
352            ],
353        );
354        assert!(entry.contains("field1"));
355        assert!(entry.contains("value1"));
356        assert!(entry.contains("field2"));
357        assert!(entry.contains("value2"));
358        assert!(entry.contains("field3"));
359        assert!(entry.contains("value3"));
360    }
361
362    #[test]
363    fn test_format_json_entry_is_valid_json() {
364        let entry = format_json_entry(Level::Info, "test", &[("key", "value")]);
365        // Should be parseable as JSON
366        let parsed: serde_json::Value = serde_json::from_str(&entry).unwrap();
367        assert_eq!(parsed["level"], "info");
368        assert_eq!(parsed["message"], "test");
369        assert!(parsed.get("timestamp").is_some());
370        assert_eq!(parsed["key"], "value");
371    }
372
373    #[test]
374    fn test_format_json_entry_timestamp_format() {
375        let entry = format_json_entry(Level::Info, "test", &[]);
376        let parsed: serde_json::Value = serde_json::from_str(&entry).unwrap();
377        let timestamp = parsed["timestamp"].as_str().unwrap();
378        // Should be RFC3339 format (contains date-time separator T or space)
379        // chrono::Utc::now().to_rfc3339() produces format like "2024-01-01T12:00:00Z"
380        assert!(!timestamp.is_empty());
381        // Should contain year, month, day
382        assert!(timestamp.contains("-"));
383    }
384}