Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-core/src/logging.rs
StratoSDK / crates / strato-core / src / logging.rs
1//! Logging system for StratoUI framework
2//!
3//! This module provides a comprehensive logging system with rate limiting,
4//! contextual error information, and category-based filtering.
5 
6use crate::config::LoggingConfig;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::sync::{Arc, OnceLock, RwLock};
10use std::time::{Duration, Instant};
11 
12/// Log levels supported by the system
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
14pub enum LogLevel {
15 Trace,
16 Debug,
17 Info,
18 Warn,
19 Error,
20}
21 
22/// Log categories for organizing log messages
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
24pub enum LogCategory {
25 Core,
26 Renderer,
27 Vulkan,
28 Text,
29 UI,
30 Input,
31 Audio,
32 Network,
33 Plugin,
34 Platform,
35}
36 
37impl LogCategory {
38 /// Convert LogCategory to string
39 pub fn as_str(&self) -> &'static str {
40 match self {
41 LogCategory::Core => "core",
42 LogCategory::Renderer => "renderer",
43 LogCategory::Vulkan => "vulkan",
44 LogCategory::Text => "text",
45 LogCategory::UI => "ui",
46 LogCategory::Input => "input",
47 LogCategory::Audio => "audio",
48 LogCategory::Network => "network",
49 LogCategory::Plugin => "plugin",
50 LogCategory::Platform => "platform",
51 }
52 }
53}
54 
55impl std::fmt::Display for LogCategory {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 write!(f, "{}", self.as_str())
58 }
59}
60 
61impl LogLevel {
62 /// Convert string to LogLevel
63 pub fn from_str(s: &str) -> Option<LogLevel> {
64 match s.to_lowercase().as_str() {
65 "trace" => Some(LogLevel::Trace),
66 "debug" => Some(LogLevel::Debug),
67 "info" => Some(LogLevel::Info),
68 "warn" => Some(LogLevel::Warn),
69 "error" => Some(LogLevel::Error),
70 _ => None,
71 }
72 }
73 
74 /// Convert LogLevel to string
75 pub fn as_str(&self) -> &'static str {
76 match self {
77 LogLevel::Trace => "trace",
78 LogLevel::Debug => "debug",
79 LogLevel::Info => "info",
80 LogLevel::Warn => "warn",
81 LogLevel::Error => "error",
82 }
83 }
84}
85 
86impl std::fmt::Display for LogLevel {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 write!(f, "{}", self.as_str())
89 }
90}
91 
92/// Rate limiting state for a specific category
93#[derive(Debug)]
94struct RateLimitState {
95 last_reset: Instant,
96 count: u32,
97 max_count: u32,
98 duration: Duration,
99}
100 
101impl RateLimitState {
102 fn new(max_count: u32, duration: Duration) -> Self {
103 Self {
104 last_reset: Instant::now(),
105 count: 0,
106 max_count,
107 duration,
108 }
109 }
110 
111 fn should_allow(&mut self) -> bool {
112 let now = Instant::now();
113 
114 // Reset counter if duration has passed
115 if now.duration_since(self.last_reset) >= self.duration {
116 self.last_reset = now;
117 self.count = 0;
118 }
119 
120 if self.count < self.max_count {
121 self.count += 1;
122 true
123 } else {
124 false
125 }
126 }
127}
128 
129/// Logger configuration and state
130#[derive(Debug)]
131pub struct LoggerConfig {
132 rate_limiters: Arc<RwLock<HashMap<String, RateLimitState>>>,
133 config: LoggingConfig,
134}
135 
136impl LoggerConfig {
137 /// Create a new logger configuration
138 pub fn new(config: LoggingConfig) -> Self {
139 Self {
140 rate_limiters: Arc::new(RwLock::new(HashMap::new())),
141 config,
142 }
143 }
144 
145 /// Check if a log message should be allowed based on rate limiting
146 pub fn should_allow_log(&self, category: &str) -> bool {
147 let mut limiters = self.rate_limiters.write().unwrap();
148 
149 let limiter = limiters.entry(category.to_string()).or_insert_with(|| {
150 RateLimitState::new(
151 self.config.max_rate_limit_count,
152 Duration::from_secs(self.config.rate_limit_seconds),
153 )
154 });
155 
156 limiter.should_allow()
157 }
158 
159 /// Check if a log level is enabled for a category
160 pub fn is_level_enabled(&self, category: &str, level: LogLevel) -> bool {
161 if let Some(category_level_str) = self.config.category_levels.get(category) {
162 if let Some(category_level) = LogLevel::from_str(category_level_str) {
163 return level >= category_level;
164 }
165 }
166 
167 // Default to Info level if category not found
168 level >= LogLevel::Info
169 }
170 
171 /// Update the configuration
172 pub fn update_config(&mut self, config: LoggingConfig) {
173 self.config = config;
174 // Clear rate limiters to apply new settings
175 self.rate_limiters.write().unwrap().clear();
176 }
177}
178 
179/// Global logger instance
180static LOGGER: OnceLock<Arc<RwLock<LoggerConfig>>> = OnceLock::new();
181 
182/// Initialize the logging system
183pub fn init(config: &LoggingConfig) -> Result<(), Box<dyn std::error::Error>> {
184 let logger_config = LoggerConfig::new(config.clone());
185 LOGGER.set(Arc::new(RwLock::new(logger_config))).ok();
186 Ok(())
187}
188 
189/// Get the global logger instance
190fn get_logger() -> Option<Arc<RwLock<LoggerConfig>>> {
191 LOGGER.get().cloned()
192}
193 
194/// Internal logging function
195pub fn log_internal(level: LogLevel, category: &str, message: &str, rate_limited: bool) {
196 if let Some(logger) = get_logger() {
197 let logger_guard = logger.read().unwrap();
198 
199 // Check if level is enabled for this category
200 if !logger_guard.is_level_enabled(category, level) {
201 return;
202 }
203 
204 // Check rate limiting if requested
205 if rate_limited && !logger_guard.should_allow_log(category) {
206 return;
207 }
208 
209 drop(logger_guard); // Release the lock before printing
210 
211 // Format and print the log message
212 let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S%.3f");
213 println!(
214 "[{}] [{}] [{}] {}",
215 timestamp,
216 level.as_str().to_uppercase(),
217 category,
218 message
219 );
220 } else {
221 // Fallback if logger not initialized
222 println!(
223 "[UNINITIALIZED] [{}] [{}] {}",
224 level.as_str().to_uppercase(),
225 category,
226 message
227 );
228 }
229}
230 
231/// Update logger configuration
232pub fn update_config(config: LoggingConfig) {
233 if let Some(logger) = get_logger() {
234 let mut logger_guard = logger.write().unwrap();
235 logger_guard.update_config(config);
236 }
237}
238 
239// Core logging macros
240#[macro_export]
241macro_rules! strato_trace {
242 ($category:expr, $($arg:tt)*) => {
243 $crate::logging::log_internal($crate::logging::LogLevel::Trace, &$category.to_string(), &format!($($arg)*), false);
244 };
245}
246 
247#[macro_export]
248macro_rules! strato_debug {
249 ($category:expr, $($arg:tt)*) => {
250 $crate::logging::log_internal($crate::logging::LogLevel::Debug, &$category.to_string(), &format!($($arg)*), false);
251 };
252}
253 
254#[macro_export]
255macro_rules! strato_info {
256 ($category:expr, $($arg:tt)*) => {
257 $crate::logging::log_internal($crate::logging::LogLevel::Info, &$category.to_string(), &format!($($arg)*), false);
258 };
259}
260 
261#[macro_export]
262macro_rules! strato_warn {
263 ($category:expr, $($arg:tt)*) => {
264 $crate::logging::log_internal($crate::logging::LogLevel::Warn, &$category.to_string(), &format!($($arg)*), false);
265 };
266}
267 
268#[macro_export]
269macro_rules! strato_error {
270 ($category:expr, $($arg:tt)*) => {
271 $crate::logging::log_internal($crate::logging::LogLevel::Error, &$category.to_string(), &format!($($arg)*), false);
272 };
273}
274 
275// Rate-limited logging macros
276#[macro_export]
277macro_rules! strato_trace_rate_limited {
278 ($category:expr, $($arg:tt)*) => {
279 $crate::logging::log_internal($crate::logging::LogLevel::Trace, &$category.to_string(), &format!($($arg)*), true);
280 };
281}
282 
283#[macro_export]
284macro_rules! strato_debug_rate_limited {
285 ($category:expr, $($arg:tt)*) => {
286 $crate::logging::log_internal($crate::logging::LogLevel::Debug, &$category.to_string(), &format!($($arg)*), true);
287 };
288}
289 
290#[macro_export]
291macro_rules! strato_info_rate_limited {
292 ($category:expr, $($arg:tt)*) => {
293 $crate::logging::log_internal($crate::logging::LogLevel::Info, &$category.to_string(), &format!($($arg)*), true);
294 };
295}
296 
297#[macro_export]
298macro_rules! strato_warn_rate_limited {
299 ($category:expr, $($arg:tt)*) => {
300 $crate::logging::log_internal($crate::logging::LogLevel::Warn, &$category.to_string(), &format!($($arg)*), true);
301 };
302}
303 
304#[macro_export]
305macro_rules! strato_error_rate_limited {
306 ($category:expr, $($arg:tt)*) => {
307 $crate::logging::log_internal($crate::logging::LogLevel::Error, &$category.to_string(), &format!($($arg)*), true);
308 };
309}
310 
311// Category-specific macros
312#[macro_export]
313macro_rules! strato_text_debug {
314 ($($arg:tt)*) => {
315 $crate::logging::log_internal($crate::logging::LogLevel::Debug, "text", &format!($($arg)*), false);
316 };
317}
318 
319// Re-export macros for easier use
320pub use strato_debug;
321pub use strato_debug_rate_limited;
322pub use strato_error;
323pub use strato_error_rate_limited;
324pub use strato_info;
325pub use strato_info_rate_limited;
326pub use strato_text_debug;
327pub use strato_trace;
328pub use strato_trace_rate_limited;
329pub use strato_warn;
330pub use strato_warn_rate_limited;
331 
332#[cfg(test)]
333mod tests {
334 use super::*;
335 use std::collections::HashMap;
336 
337 #[test]
338 fn test_log_level_conversion() {
339 assert_eq!(LogLevel::from_str("info"), Some(LogLevel::Info));
340 assert_eq!(LogLevel::from_str("INFO"), Some(LogLevel::Info));
341 assert_eq!(LogLevel::from_str("invalid"), None);
342 
343 assert_eq!(LogLevel::Info.as_str(), "info");
344 assert_eq!(LogLevel::Error.as_str(), "error");
345 }
346 
347 #[test]
348 fn test_rate_limiting() {
349 let mut state = RateLimitState::new(2, Duration::from_millis(100));
350 
351 // First two should be allowed
352 assert!(state.should_allow());
353 assert!(state.should_allow());
354 
355 // Third should be blocked
356 assert!(!state.should_allow());
357 
358 // After waiting, should be allowed again
359 std::thread::sleep(Duration::from_millis(150));
360 assert!(state.should_allow());
361 }
362 
363 #[test]
364 fn test_logger_config() {
365 let mut category_levels = HashMap::new();
366 category_levels.insert("test".to_string(), "debug".to_string());
367 
368 let config = LoggingConfig {
369 category_levels,
370 enable_text_debug: true,
371 enable_layout_debug: false,
372 rate_limit_seconds: 1,
373 max_rate_limit_count: 5,
374 };
375 
376 let logger_config = LoggerConfig::new(config);
377 
378 // Test level checking
379 assert!(logger_config.is_level_enabled("test", LogLevel::Debug));
380 assert!(logger_config.is_level_enabled("test", LogLevel::Error));
381 assert!(!logger_config.is_level_enabled("test", LogLevel::Trace));
382 
383 // Test rate limiting
384 assert!(logger_config.should_allow_log("test"));
385 }
386}
387