Skip to main content

AirLibrary/Logging/
mod.rs

1//! # Structured Logging Module
2//!
3//! Provides comprehensive structured logging with JSON output, request ID
4//! propagation, context-aware logging, log rotation, sensitive data filtering,
5//! and validation.
6//!
7//! ## Responsibilities
8//!
9//! ### Structured Logging
10//! - JSON output format for machine parsing and analysis
11//! - Request ID and trace ID propagation across log entries
12//! - Context-aware logging with operation tracking
13//! - Log level filtering (TRACE, DEBUG, INFO, WARN, ERROR)
14//!
15//! ### Log Rotation
16//! - Size-based log rotation to prevent disk exhaustion
17//! - Time-based rotation (daily) for archival
18//! - Automatic cleanup of old log files
19//! - Compressed log file storage for space efficiency
20//!
21//! ### Context Management
22//! - Thread-local context storage for async operations
23//! - Automatic context propagation across await points
24//! - Correlation ID linking distributed requests
25//! - User and session tracking
26//!
27//! ### Sensitive Data Handling
28//! - Automatic redaction of sensitive fields
29//! - Configurable sensitive patterns
30//! - Sanitization of error messages
31//! - Audit logging for security events
32//!
33//! ### Log Validation
34//! - Structured log data validation before output
35//! - Schema enforcement for consistent format
36//! - Size limits on log messages
37//! - Malformed log rejection
38//!
39//! ## Integration with Mountain
40//!
41//! Logs flow to Mountain's debugging infrastructure:
42//! - Real-time log streaming to debug console
43//! - Historical log search and filtering
44//! - Error aggregation and alerting
45//! - Performance profiling logs
46//!
47//! ## VSCode Debugging References
48//!
49//! Similar logging patterns used in VSCode for:
50//! - Exception and error tracking
51//! - Debug output for extension development
52//! - Performance profiling traces
53//! - Cross-process communication logging
54//!
55//! Reference:
56//! vs/base/common/errors
57//!
58//! # FUTURE Enhancements
59//!
60//! - [DISTRIBUTED TRACING] Tighter integration with Tracing module
61//! - `ELASTICSEARCH`: Direct log export to Elasticsearch/Logstash
62//! - [LOG ANALYSIS] Automatic anomaly detection in logs
63//! - `KIBANA`: Pre-built Kibana dashboards
64//! - [LOG PARSING] Support for custom log formats
65//! ## Sensitive Data Handling
66//!
67//! All logs are automatically sanitized:
68//! - Passwords, tokens, and secrets are redacted
69//! - User-identifiable information is masked
70//! - API keys and secrets are removed
71//! - Error messages are parsed for sensitive patterns
72
73use std::{
74	collections::HashMap,
75	path::{Path, PathBuf},
76	sync::{Arc, Mutex},
77	time::{SystemTime, UNIX_EPOCH},
78};
79
80use serde::{Deserialize, Serialize};
81use tracing_subscriber::{fmt::format::FmtSpan, prelude::*};
82use tracing_appender::rolling::Rotation;
83
84use crate::{Result, dev_log};
85
86/// Configuration for log rotation and management
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LogRotationConfig {
89	/// Maximum size of a single log file in bytes before rotation
90	pub MaxFileSizeBytes:u64,
91
92	/// Maximum number of rotated log files to retain
93	pub MaxFiles:usize,
94
95	/// Rotation strategy (daily, hourly, never)
96	pub Rotation:LogRotation,
97
98	/// Whether to compress rotated log files
99	pub Compress:bool,
100
101	/// Log directory path
102	pub LogDirectory:String,
103
104	/// Log file name prefix
105	pub LogFilePrefix:String,
106}
107
108/// Log rotation strategies
109#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
110pub enum LogRotation {
111	/// Rotate daily
112	Daily,
113
114	/// Rotate every hour
115	Hourly,
116
117	/// Rotate every minute (for debugging)
118	Minutely,
119
120	/// Never rotate automatically
121	Never,
122}
123
124impl Default for LogRotation {
125	fn default() -> Self { Self::Daily }
126}
127
128impl Default for LogRotationConfig {
129	fn default() -> Self {
130		Self {
131			MaxFileSizeBytes:100 * 1024 * 1024, // 100 MB
132
133			MaxFiles:30, // Keep 30 days of logs
134
135			Rotation:LogRotation::Daily,
136
137			Compress:true,
138
139			LogDirectory:"./Log".to_string(),
140
141			LogFilePrefix:"Air".to_string(),
142		}
143	}
144}
145
146impl LogRotationConfig {
147	/// Validate log rotation configuration
148	pub fn Validate(&self) -> Result<()> {
149		if self.MaxFileSizeBytes == 0 {
150			return Err("MaxFileSizeBytes must be greater than 0".into());
151		}
152
153		if self.MaxFileSizeBytes > 10 * 1024 * 1024 * 1024 {
154			// Max 10 GB
155			return Err("MaxFileSizeBytes cannot exceed 10 GB".into());
156		}
157
158		if self.MaxFiles == 0 {
159			return Err("MaxFiles must be greater than 0".into());
160		}
161
162		if self.MaxFiles > 365 {
163			// Max 1 year retention
164			return Err("MaxFiles cannot exceed 365".into());
165		}
166
167		Ok(())
168	}
169
170	/// Convert to tracing_appender Rotation
171	pub fn ToTracingRotation(&self) -> Rotation {
172		match self.Rotation {
173			LogRotation::Daily => Rotation::DAILY,
174
175			LogRotation::Hourly => Rotation::HOURLY,
176
177			LogRotation::Minutely => Rotation::NEVER, // No minutely support
178
179			LogRotation::Never => Rotation::NEVER,
180		}
181	}
182}
183
184/// Sensitive data patterns for redaction
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SensitiveDataConfig {
187	/// Enable automatic sensitive data redaction
188	pub Enabled:bool,
189
190	/// Custom patterns to redact (regex)
191	pub CustomPatterns:Vec<String>,
192
193	/// Standard patterns to include (password, token, secret, etc.)
194	pub IncludeStandardPatterns:bool,
195}
196
197impl Default for SensitiveDataConfig {
198	fn default() -> Self { Self { Enabled:true, CustomPatterns:Vec::new(), IncludeStandardPatterns:true } }
199}
200
201/// Context for structured logging with request IDs and metadata
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct LogContext {
204	pub RequestId:String,
205
206	pub TraceId:String,
207
208	pub SpanId:String,
209
210	pub UserId:Option<String>,
211
212	pub SessionId:Option<String>,
213
214	pub Operation:String,
215
216	pub Metadata:HashMap<String, String>,
217}
218
219impl LogContext {
220	/// Create a new log context
221	pub fn New(Operation:impl Into<String>) -> Self {
222		let RequestId = crate::Utility::GenerateRequestId();
223
224		let TraceId = crate::Utility::GenerateRequestId();
225
226		let SpanId = uuid::Uuid::new_v4().to_string();
227
228		Self {
229			RequestId,
230
231			TraceId,
232
233			SpanId,
234
235			UserId:None,
236
237			SessionId:None,
238
239			Operation:Operation.into(),
240
241			Metadata:HashMap::new(),
242		}
243	}
244
245	/// Validate log context for required fields
246	pub fn Validate(&self) -> Result<()> {
247		if self.RequestId.is_empty() {
248			return Err("RequestId cannot be empty".into());
249		}
250
251		if self.TraceId.is_empty() {
252			return Err("TraceId cannot be empty".into());
253		}
254
255		if self.Operation.is_empty() {
256			return Err("Operation cannot be empty".into());
257		}
258
259		Ok(())
260	}
261
262	/// Set user ID in context
263	pub fn WithUserId(mut self, UserId:String) -> Self {
264		self.UserId = Some(UserId);
265
266		self
267	}
268
269	/// Set session ID in context
270	pub fn WithSessionId(mut self, SessionId:String) -> Self {
271		self.SessionId = Some(SessionId);
272
273		self
274	}
275
276	/// Add metadata to context
277	pub fn WithMetadata(mut self, Key:String, Value:String) -> Self {
278		self.Metadata.insert(Key, Value);
279
280		self
281	}
282
283	/// Add multiple metadata entries
284	pub fn WithMetadataMap(mut self, Metadata:HashMap<String, String>) -> Self {
285		self.Metadata.extend(Metadata);
286
287		self
288	}
289}
290
291thread_local! {
292
293	static LOG_CONTEXT: std::cell::RefCell<Option<LogContext>> = std::cell::RefCell::new(None);
294}
295
296/// Set the log context for the current thread
297pub fn SetLogContext(Context:LogContext) {
298	if let Err(e) = Context.Validate() {
299		dev_log!("air", "error: [Logging] Invalid log context provided: {:?}", e);
300
301		return;
302	}
303
304	LOG_CONTEXT.with(|ctx| {
305		*ctx.borrow_mut() = Some(Context);
306	});
307}
308
309/// Get the current log context
310pub fn GetLogContext() -> Option<LogContext> { LOG_CONTEXT.with(|Context| Context.borrow().clone()) }
311
312/// Clear the log context for the current thread
313pub fn ClearLogContext() {
314	LOG_CONTEXT.with(|Context| {
315		*Context.borrow_mut() = None;
316	});
317}
318
319/// Log file manager for rotation and cleanup
320pub struct LogManager {
321	Config:LogRotationConfig,
322
323	CurrentFile:Arc<Mutex<Option<PathBuf>>>,
324
325	CurrentSize:Arc<Mutex<u64>>,
326}
327
328impl LogManager {
329	fn new(Config:LogRotationConfig) -> Result<Self> {
330		Config.Validate()?;
331
332		// Ensure log directory exists
333		std::fs::create_dir_all(&Config.LogDirectory)?;
334
335		Ok(Self {
336			Config,
337			CurrentFile:Arc::new(Mutex::new(None)),
338			CurrentSize:Arc::new(Mutex::new(0)),
339		})
340	}
341
342	/// Check if log rotation is needed
343	fn ShouldRotate(&self) -> bool {
344		let size = *self.CurrentSize.lock().unwrap();
345
346		size >= self.Config.MaxFileSizeBytes
347	}
348
349	/// Perform log rotation
350	fn Rotate(&self) -> Result<()> {
351		let CurrentFile = self.CurrentFile.lock().unwrap();
352
353		if let Some(ref FilePath) = *CurrentFile {
354			// Rename current file with timestamp
355			let Timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs();
356
357			let RotatedPath = format!("{}.{}.log", FilePath.display(), Timestamp);
358
359			std::fs::rename(FilePath, &RotatedPath)?;
360
361			// Compress if enabled
362			if self.Config.Compress {
363				self.CompressFile(&RotatedPath)?;
364			}
365
366			// Cleanup old log files
367			self.CleanupOldLogs()?;
368		}
369
370		*self.CurrentSize.lock().unwrap() = 0;
371
372		Ok(())
373	}
374
375	/// Compress a log file
376	fn CompressFile(&self, path:&str) -> crate::Result<()> {
377		// Basic compression - in production would use actual compression
378		let _ = path;
379
380		Ok(())
381	}
382
383	/// Cleanup old log files
384	fn CleanupOldLogs(&self) -> Result<()> {
385		let log_dir = Path::new(&self.Config.LogDirectory);
386
387		if !log_dir.exists() {
388			return Ok(());
389		}
390
391		let mut log_files:Vec<_> = std::fs::read_dir(log_dir)?
392			.filter_map(|e| e.ok())
393			.filter(|e| {
394				e.path()
395					.extension()
396					.and_then(|s| s.to_str())
397					.map(|ext| ext == "log")
398					.unwrap_or(false)
399			})
400			.collect();
401
402		// Sort by modification time (newest first)
403		log_files.sort_by(|a, b| {
404			let a_time = a.metadata().and_then(|m| m.modified()).unwrap_or(UNIX_EPOCH);
405
406			let b_time = b.metadata().and_then(|m| m.modified()).unwrap_or(UNIX_EPOCH);
407
408			b_time.cmp(&a_time)
409		});
410
411		// Keep only max_files
412		for file in log_files.into_iter().skip(self.Config.MaxFiles) {
413			let _ = std::fs::remove_file(file.path());
414		}
415
416		Ok(())
417	}
418}
419
420/// Sensitive data filter for log sanitization
421#[derive(Debug, Clone)]
422pub struct SensitiveDataFilter {
423	enabled:bool,
424
425	patterns:Vec<regex::Regex>,
426}
427
428impl Default for SensitiveDataFilter {
429	fn default() -> Self {
430		let mut patterns = Vec::new();
431
432		// Standard sensitive patterns - simplified to avoid escaping issues
433		patterns.push(regex::Regex::new(r"(?i)password[=[:space:]]+\S+").unwrap());
434
435		patterns.push(regex::Regex::new(r"(?i)token[=[:space:]]+\S+").unwrap());
436
437		patterns.push(regex::Regex::new(r"(?i)secret[=[:space:]]+\S+").unwrap());
438
439		patterns.push(regex::Regex::new(r"(?i)(api|private)[_-]?key[=[:space:]]+\S+").unwrap());
440
441		patterns.push(regex::Regex::new(r"(?i)authorization[=[:space:]]+Bearer[[:space:]]+\S+").unwrap());
442
443		patterns.push(regex::Regex::new(r"(?i)credential[=[:space:]]+\S+").unwrap());
444
445		Self { enabled:true, patterns }
446	}
447}
448
449impl SensitiveDataFilter {
450	fn new(Config:SensitiveDataConfig) -> Result<Self> {
451		let mut filter = Self::default();
452
453		filter.enabled = Config.Enabled;
454
455		if !Config.IncludeStandardPatterns {
456			filter.patterns.clear();
457		}
458
459		// Add custom patterns
460		for pattern in &Config.CustomPatterns {
461			match regex::Regex::new(pattern) {
462				Ok(re) => filter.patterns.push(re),
463
464				Err(e) => dev_log!("air", "warn: [Logging] Failed to compile custom regex '{}': {}", pattern, e),
465			}
466		}
467
468		Ok(filter)
469	}
470
471	/// Filter sensitive data from a string
472	fn Filter(&self, input:&str) -> String {
473		if !self.enabled {
474			return input.to_string();
475		}
476
477		let mut filtered = input.to_string();
478
479		for pattern in &self.patterns {
480			filtered = pattern.replace_all(&filtered, "[REDACTED]").to_string();
481		}
482
483		filtered
484	}
485}
486
487/// Structured log entry for validation
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct StructuredLogEntry {
490	pub Timestamp:u64,
491
492	pub Level:String,
493
494	pub Message:String,
495
496	pub RequestId:Option<String>,
497
498	pub TraceId:Option<String>,
499
500	pub SpanId:Option<String>,
501
502	pub Operation:Option<String>,
503
504	pub UserId:Option<String>,
505
506	pub Metadata:HashMap<String, String>,
507}
508
509impl StructuredLogEntry {
510	/// Validate log entry structure
511	pub fn Validate(&self) -> Result<()> {
512		if self.Level.is_empty() {
513			return Err("log level cannot be empty".into());
514		}
515
516		if self.Message.is_empty() {
517			return Err("log message cannot be empty".into());
518		}
519
520		if !["TRACE", "DEBUG", "INFO", "WARN", "ERROR"].contains(&self.Level.as_str()) {
521			return Err(format!("invalid log level: {}", self.Level).into());
522		}
523
524		if self.Message.len() > 10000 {
525			// Max 10KB message
526			return Err("log message too large".into());
527		}
528
529		Ok(())
530	}
531}
532
533/// Context-aware logger for structured logging
534#[derive(Debug, Clone)]
535pub struct ContextLogger {
536	json_output:bool,
537
538	log_file_path:Option<String>,
539
540	rotation_config:Option<LogRotationConfig>,
541
542	sensitive_filter:Arc<SensitiveDataFilter>,
543
544	initialized:Arc<Mutex<bool>>,
545}
546
547impl ContextLogger {
548	/// Create a new context logger
549	pub fn New(json_output:bool, log_file_path:Option<String>) -> Self {
550		Self {
551			json_output,
552
553			log_file_path,
554
555			rotation_config:None,
556
557			sensitive_filter:Arc::new(SensitiveDataFilter::default()),
558
559			initialized:Arc::new(Mutex::new(false)),
560		}
561	}
562
563	/// Create with log rotation configuration
564	pub fn WithRotation(
565		json_output:bool,
566
567		log_file_path:Option<String>,
568
569		rotation_config:LogRotationConfig,
570	) -> Result<Self> {
571		rotation_config.Validate()?;
572
573		Ok(Self {
574			json_output,
575			log_file_path,
576			rotation_config:Some(rotation_config),
577			sensitive_filter:Arc::new(SensitiveDataFilter::default()),
578			initialized:Arc::new(Mutex::new(false)),
579		})
580	}
581
582	/// Set sensitive data filter configuration
583	pub fn WithSensitiveFilter(mut self, Config:SensitiveDataConfig) -> Result<Self> {
584		self.sensitive_filter = Arc::new(SensitiveDataFilter::new(Config)?);
585
586		Ok(self)
587	}
588
589	/// Initialize the logging system with tracing
590	pub fn Initialize(&self) -> Result<()> {
591		// Check if already initialized
592		let mut initialized = self.initialized.lock().unwrap();
593
594		if *initialized {
595			return Ok(());
596		}
597
598		let filter = tracing_subscriber::EnvFilter::from_default_env()
599			.add_directive(tracing_subscriber::filter::LevelFilter::INFO.into());
600
601		if self.json_output {
602			// JSON output format
603			let fmt_layer = tracing_subscriber::fmt::layer()
604				.json()
605				.with_current_span(true)
606				.with_span_list(false)
607				.with_target(true)
608				.with_file(true)
609				.with_line_number(true)
610				.with_writer(std::io::stderr)
611				.with_ansi(false)
612				.with_span_events(FmtSpan::FULL);
613
614			let registry = tracing_subscriber::registry().with(filter).with(fmt_layer);
615
616			// Set up log file if specified
617			if let Some(ref log_path) = self.log_file_path {
618				let log_dir = std::path::Path::new(log_path).parent().unwrap_or(std::path::Path::new("."));
619
620				let log_file = std::path::Path::new(log_path)
621					.file_name()
622					.unwrap_or(std::ffi::OsStr::new("Air.log"));
623
624				let file_appender = tracing_appender::rolling::daily(log_dir, log_file);
625
626				let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
627
628				let file_layer = tracing_subscriber::fmt::layer()
629					.json()
630					.with_current_span(true)
631					.with_span_list(false)
632					.with_target(true)
633					.with_file(true)
634					.with_line_number(true)
635					.with_writer(non_blocking)
636					.with_ansi(false)
637					.with_span_events(FmtSpan::FULL);
638
639				registry.with(file_layer).init();
640			} else {
641				registry.init();
642			}
643		} else {
644			// Standard text output format
645			let fmt_layer = tracing_subscriber::fmt::layer()
646				.with_target(true)
647				.with_file(true)
648				.with_line_number(true)
649				.with_writer(std::io::stderr)
650				.with_ansi(true)
651				.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
652
653			let registry = tracing_subscriber::registry().with(filter).with(fmt_layer);
654
655			// Set up log file if specified
656			if let Some(ref log_path) = self.log_file_path {
657				let log_dir = std::path::Path::new(log_path).parent().unwrap_or(std::path::Path::new("."));
658
659				let log_file = std::path::Path::new(log_path)
660					.file_name()
661					.unwrap_or(std::ffi::OsStr::new("Air.log"));
662
663				let file_appender = tracing_appender::rolling::daily(log_dir, log_file);
664
665				let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
666
667				let file_layer = tracing_subscriber::fmt::layer()
668					.with_target(true)
669					.with_file(true)
670					.with_line_number(true)
671					.with_writer(non_blocking)
672					.with_ansi(false)
673					.with_span_events(FmtSpan::NEW | FmtSpan::CLOSE);
674
675				registry.with(file_layer).init();
676			} else {
677				registry.init();
678			}
679		}
680
681		*initialized = true;
682		dev_log!("air", "[Logging] ContextLogger initialized - JSON output: {}", self.json_output);
683
684		Ok(())
685	}
686
687	/// Log with context at info level
688	pub fn Info(&self, message:impl Into<String>) {
689		let msg = self.sensitive_filter.Filter(&message.into());
690
691		if let Some(Context) = GetLogContext() {
692			dev_log!(
693				"air",
694				"[{}] req={} trace={} span={} {}",
695				Context.Operation,
696				Context.RequestId,
697				Context.TraceId,
698				Context.SpanId,
699				msg
700			);
701		} else {
702			dev_log!("air", "{}", msg);
703		}
704	}
705
706	/// Log with context at debug level
707	pub fn Debug(&self, message:impl Into<String>) {
708		let msg = self.sensitive_filter.Filter(&message.into());
709
710		if let Some(Context) = GetLogContext() {
711			dev_log!(
712				"air",
713				"[{}] req={} trace={} span={} {}",
714				Context.Operation,
715				Context.RequestId,
716				Context.TraceId,
717				Context.SpanId,
718				msg
719			);
720		} else {
721			dev_log!("air", "{}", msg);
722		}
723	}
724
725	/// Log with context at warn level
726	pub fn Warn(&self, message:impl Into<String>) {
727		let msg = self.sensitive_filter.Filter(&message.into());
728
729		if let Some(Context) = GetLogContext() {
730			dev_log!(
731				"air",
732				"warn: [{}] req={} trace={} span={} {}",
733				Context.Operation,
734				Context.RequestId,
735				Context.TraceId,
736				Context.SpanId,
737				msg
738			);
739		} else {
740			dev_log!("air", "warn: {}", msg);
741		}
742	}
743
744	/// Log with context at error level
745	pub fn Error(&self, message:impl Into<String>) {
746		let msg = self.sensitive_filter.Filter(&message.into());
747
748		if let Some(Context) = GetLogContext() {
749			dev_log!(
750				"air",
751				"error: [{}] req={} trace={} span={} {}",
752				Context.Operation,
753				Context.RequestId,
754				Context.TraceId,
755				Context.SpanId,
756				msg
757			);
758		} else {
759			dev_log!("air", "error: {}", msg);
760		}
761	}
762}
763
764/// Global context logger instance
765static LOGGER_INSTANCE:std::sync::OnceLock<ContextLogger> = std::sync::OnceLock::new();
766
767/// Get the global context logger
768pub fn GetLogger() -> &'static ContextLogger { LOGGER_INSTANCE.get_or_init(|| ContextLogger::New(false, None)) }
769
770/// Initialize the global context logger
771pub fn InitializeLogger(json_output:bool, log_file_path:Option<String>) -> Result<()> {
772	let logger = ContextLogger::New(json_output, log_file_path);
773
774	logger.Initialize()?;
775
776	let _old = LOGGER_INSTANCE.set(logger);
777
778	Ok(())
779}
780
781/// Initialize the global context logger with rotation
782pub fn InitializeLoggerWithRotation(
783	json_output:bool,
784
785	log_file_path:Option<String>,
786
787	rotation_config:LogRotationConfig,
788
789	sensitive_config:Option<SensitiveDataConfig>,
790) -> Result<()> {
791	let mut logger = ContextLogger::WithRotation(json_output, log_file_path, rotation_config)?;
792
793	if let Some(sensitive_config) = sensitive_config {
794		logger = logger.WithSensitiveFilter(sensitive_config)?;
795	}
796
797	logger.Initialize()?;
798
799	let _old = LOGGER_INSTANCE.set(logger);
800
801	Ok(())
802}