Line data Source code
1 : use std::fs::OpenOptions;
2 : use std::io::Write;
3 : use std::sync::{Mutex, OnceLock};
4 :
5 : #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
6 : pub enum LogLevel {
7 : Error = 1,
8 : Warn = 2,
9 : Info = 3,
10 : Debug = 4,
11 : Trace = 5,
12 : }
13 :
14 : impl LogLevel {
15 16 : fn from_str(input: &str) -> Option<Self> {
16 16 : match input.to_ascii_lowercase().as_str() {
17 16 : "error" => Some(Self::Error),
18 14 : "warn" | "warning" => Some(Self::Warn),
19 12 : "info" => Some(Self::Info),
20 10 : "debug" => Some(Self::Debug),
21 8 : "trace" => Some(Self::Trace),
22 4 : _ => None,
23 : }
24 16 : }
25 :
26 10 : fn as_str(self) -> &'static str {
27 10 : match self {
28 2 : Self::Error => "ERROR",
29 2 : Self::Warn => "WARN",
30 2 : Self::Info => "INFO",
31 2 : Self::Debug => "DEBUG",
32 2 : Self::Trace => "TRACE",
33 : }
34 10 : }
35 : }
36 :
37 : #[derive(Clone, Debug)]
38 : struct LoggerConfig {
39 : enabled: bool,
40 : level: LogLevel,
41 : file_path: String,
42 : filtered_events_enabled: bool,
43 : }
44 :
45 : static LOGGER_CONFIG: OnceLock<LoggerConfig> = OnceLock::new();
46 : static LOG_WRITE_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
47 :
48 8 : fn read_config() -> LoggerConfig {
49 8 : let enabled = std::env::var("HUNKY_LOG")
50 8 : .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
51 8 : .unwrap_or(false);
52 :
53 8 : let level = std::env::var("HUNKY_LOG_LEVEL")
54 8 : .ok()
55 8 : .as_deref()
56 8 : .and_then(LogLevel::from_str)
57 8 : .unwrap_or(LogLevel::Info);
58 :
59 8 : let file_path = std::env::var("HUNKY_LOG_FILE").unwrap_or_else(|_| "hunky.log".to_string());
60 :
61 8 : let filtered_events_enabled = std::env::var("HUNKY_LOG_FILTERED_EVENTS")
62 8 : .map(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes" | "on"))
63 8 : .unwrap_or(false);
64 :
65 8 : LoggerConfig {
66 8 : enabled,
67 8 : level,
68 8 : file_path,
69 8 : filtered_events_enabled,
70 8 : }
71 8 : }
72 :
73 61 : fn config() -> &'static LoggerConfig {
74 61 : LOGGER_CONFIG.get_or_init(read_config)
75 61 : }
76 :
77 0 : pub fn init() {
78 0 : let _ = config();
79 0 : }
80 :
81 60 : pub fn enabled(level: LogLevel) -> bool {
82 60 : let cfg = config();
83 60 : cfg.enabled && level <= cfg.level
84 60 : }
85 :
86 1 : pub fn filtered_events_enabled() -> bool {
87 1 : let cfg = config();
88 1 : cfg.enabled && cfg.filtered_events_enabled
89 1 : }
90 :
91 60 : pub fn log(level: LogLevel, msg: impl AsRef<str>) {
92 60 : if !enabled(level) {
93 60 : return;
94 0 : }
95 :
96 0 : let cfg = config();
97 0 : let write_lock = LOG_WRITE_LOCK.get_or_init(|| Mutex::new(()));
98 0 : let _guard = write_lock.lock().unwrap_or_else(|e| e.into_inner());
99 :
100 0 : if let Ok(mut file) = OpenOptions::new()
101 0 : .create(true)
102 0 : .append(true)
103 0 : .open(&cfg.file_path)
104 : {
105 0 : let ts = std::time::SystemTime::now()
106 0 : .duration_since(std::time::UNIX_EPOCH)
107 0 : .map(|d| d.as_secs())
108 0 : .unwrap_or(0);
109 0 : let _ = writeln!(file, "[{}] [{}] {}", ts, level.as_str(), msg.as_ref());
110 0 : }
111 60 : }
112 :
113 : #[allow(dead_code)]
114 0 : pub fn error(msg: impl AsRef<str>) {
115 0 : log(LogLevel::Error, msg);
116 0 : }
117 :
118 0 : pub fn warn(msg: impl AsRef<str>) {
119 0 : log(LogLevel::Warn, msg);
120 0 : }
121 :
122 : #[allow(dead_code)]
123 0 : pub fn info(msg: impl AsRef<str>) {
124 0 : log(LogLevel::Info, msg);
125 0 : }
126 :
127 60 : pub fn debug(msg: impl AsRef<str>) {
128 60 : log(LogLevel::Debug, msg);
129 60 : }
130 :
131 0 : pub fn trace(msg: impl AsRef<str>) {
132 0 : log(LogLevel::Trace, msg);
133 0 : }
134 :
135 : #[cfg(test)]
136 : #[path = "../tests/logger.rs"]
137 : mod tests;
|