1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct LogRotationConfig {
89 pub MaxFileSizeBytes:u64,
91
92 pub MaxFiles:usize,
94
95 pub Rotation:LogRotation,
97
98 pub Compress:bool,
100
101 pub LogDirectory:String,
103
104 pub LogFilePrefix:String,
106}
107
108#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
110pub enum LogRotation {
111 Daily,
113
114 Hourly,
116
117 Minutely,
119
120 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, MaxFiles:30, 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 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 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 return Err("MaxFiles cannot exceed 365".into());
165 }
166
167 Ok(())
168 }
169
170 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, LogRotation::Never => Rotation::NEVER,
180 }
181 }
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct SensitiveDataConfig {
187 pub Enabled:bool,
189
190 pub CustomPatterns:Vec<String>,
192
193 pub IncludeStandardPatterns:bool,
195}
196
197impl Default for SensitiveDataConfig {
198 fn default() -> Self { Self { Enabled:true, CustomPatterns:Vec::new(), IncludeStandardPatterns:true } }
199}
200
201#[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 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 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 pub fn WithUserId(mut self, UserId:String) -> Self {
264 self.UserId = Some(UserId);
265
266 self
267 }
268
269 pub fn WithSessionId(mut self, SessionId:String) -> Self {
271 self.SessionId = Some(SessionId);
272
273 self
274 }
275
276 pub fn WithMetadata(mut self, Key:String, Value:String) -> Self {
278 self.Metadata.insert(Key, Value);
279
280 self
281 }
282
283 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
296pub 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
309pub fn GetLogContext() -> Option<LogContext> { LOG_CONTEXT.with(|Context| Context.borrow().clone()) }
311
312pub fn ClearLogContext() {
314 LOG_CONTEXT.with(|Context| {
315 *Context.borrow_mut() = None;
316 });
317}
318
319pub 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 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 fn ShouldRotate(&self) -> bool {
344 let size = *self.CurrentSize.lock().unwrap();
345
346 size >= self.Config.MaxFileSizeBytes
347 }
348
349 fn Rotate(&self) -> Result<()> {
351 let CurrentFile = self.CurrentFile.lock().unwrap();
352
353 if let Some(ref FilePath) = *CurrentFile {
354 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 if self.Config.Compress {
363 self.CompressFile(&RotatedPath)?;
364 }
365
366 self.CleanupOldLogs()?;
368 }
369
370 *self.CurrentSize.lock().unwrap() = 0;
371
372 Ok(())
373 }
374
375 fn CompressFile(&self, path:&str) -> crate::Result<()> {
377 let _ = path;
379
380 Ok(())
381 }
382
383 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 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 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#[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 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 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 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#[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 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 return Err("log message too large".into());
527 }
528
529 Ok(())
530 }
531}
532
533#[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 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 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 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 pub fn Initialize(&self) -> Result<()> {
591 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 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 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 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 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 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 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 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 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
764static LOGGER_INSTANCE:std::sync::OnceLock<ContextLogger> = std::sync::OnceLock::new();
766
767pub fn GetLogger() -> &'static ContextLogger { LOGGER_INSTANCE.get_or_init(|| ContextLogger::New(false, None)) }
769
770pub 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
781pub 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}