dmsc/log/mod.rs
1//! Copyright © 2025-2026 Wenze Wei. All Rights Reserved.
2//!
3//! This file is part of DMSC.
4//! The DMSC project belongs to the Dunimd Team.
5//!
6//! Licensed under the Apache License, Version 2.0 (the "License");
7//! You may not use this file except in compliance with the License.
8//! You may obtain a copy of the License at
9//!
10//! http://www.apache.org/licenses/LICENSE-2.0
11//!
12//! Unless required by applicable law or agreed to in writing, software
13//! distributed under the License is distributed on an "AS IS" BASIS,
14//! WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15//! See the License for the specific language governing permissions and
16//! limitations under the License.
17
18#![allow(non_snake_case)]
19
20//! # Logging System
21//!
22//! This module provides a comprehensive logging system for DMSC, supporting multiple output formats,
23//! log levels, and configurable logging behavior. It includes support for structured logging,
24//! distributed tracing integration, and log rotation.
25//!
26//! ## Key Components
27//!
28//! - **DMSCLogLevel**: Enum defining supported log levels (Debug, Info, Warn, Error)
29//! - **DMSCLogConfig**: Configuration struct for logging behavior
30//! - **DMSCLogContext**: Thread-local context for adding contextual information to logs
31//! - **DMSCLogger**: Public-facing logger class for application use
32//! - **LoggerImpl**: Internal logger implementation
33//!
34//! ## Design Principles
35//!
36//! 1. **Multiple Outputs**: Supports both console and file logging
37//! 2. **Structured Logging**: Supports both text and JSON formats
38//! 3. **Distributed Tracing**: Integrates with distributed tracing context
39//! 4. **Configurable**: Highly configurable through `DMSCLogConfig`
40//! 5. **Performance**: Includes sampling support for high-volume logging
41//! 6. **Log Rotation**: Supports size-based log rotation
42//! 7. **Contextual Logging**: Allows adding contextual information to logs
43//!
44//! ## Usage
45//!
46//! ```rust,ignore
47//! use dmsc::prelude::*;
48//!
49//! fn example() -> DMSCResult<()> {
50//! // Create a default log configuration
51//! let log_config = DMSCLogConfig::default();
52//!
53//! // Create a file system instance (usually provided by the service context)
54//! let fs = DMSCFileSystem::new();
55//!
56//! // Create a logger
57//! let logger = DMSCLogger::new(&log_config, fs);
58//!
59//! // Log messages at different levels
60//! logger.debug("example", "Debug message")?;
61//! logger.info("example", "Info message")?;
62//! logger.warn("example", "Warning message")?;
63//! logger.error("example", "Error message")?;
64//!
65//! Ok(())
66//! }
67//! ```
68
69// Logging module for DMSC.
70// This is a first-stage implementation using std only; can be extended later.
71
72use std::fmt::Debug;
73use std::sync::{Arc, Mutex, Condvar};
74use std::thread;
75use std::time::Duration;
76use std::collections::VecDeque;
77
78use crate::core::DMSCResult;
79use crate::fs::DMSCFileSystem;
80use rand;
81use serde_json::json;
82use std::fs as stdfs;
83use std::time::SystemTime;
84use std::time::UNIX_EPOCH;
85mod context;
86pub use context::DMSCLogContext;
87
88/// Log level definition.
89///
90/// This enum defines the supported log levels in DMSC, ordered by severity from lowest to highest.
91#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(eq, eq_int))]
92#[derive(Clone, Copy, Debug, PartialEq, Eq)]
93pub enum DMSCLogLevel {
94 /// Debug level: Detailed information for debugging purposes
95 Debug,
96 /// Info level: General information about application operation
97 Info,
98 /// Warn level: Warning messages about potential issues
99 Warn,
100 /// Error level: Error messages about failures
101 Error,
102}
103
104impl DMSCLogLevel {
105 /// Returns the string representation of the log level.
106 ///
107 /// # Returns
108 ///
109 /// A static string representing the log level ("DEBUG", "INFO", "WARN", or "ERROR")
110 pub fn as_str(&self) -> &'static str {
111 match self {
112 DMSCLogLevel::Debug => "DEBUG",
113 DMSCLogLevel::Info => "INFO",
114 DMSCLogLevel::Warn => "WARN",
115 DMSCLogLevel::Error => "ERROR",
116 }
117 }
118
119 /// Returns the color block emoji for the log level.
120 ///
121 /// # Returns
122 ///
123 /// A static string representing the color block emoji
124 pub fn color_block(&self) -> &'static str {
125 match self {
126 DMSCLogLevel::Debug => "🟦",
127 DMSCLogLevel::Info => "🟩",
128 DMSCLogLevel::Warn => "🟨",
129 DMSCLogLevel::Error => "🟥",
130 }
131 }
132
133 /// Parses a log level from an environment variable.
134 ///
135 /// Reads the `DMSC_LOG_LEVEL` environment variable and returns the corresponding log level.
136 /// If the environment variable is not set or contains an invalid value, returns `None`.
137 ///
138 /// # Returns
139 ///
140 /// An `Option<DMSCLogLevel>` containing the parsed log level
141 pub fn from_env() -> Option<Self> {
142 std::env::var("DMSC_LOG_LEVEL").ok().and_then(|s| {
143 match s.to_ascii_uppercase().as_str() {
144 "DEBUG" => Some(DMSCLogLevel::Debug),
145 "INFO" => Some(DMSCLogLevel::Info),
146 "WARN" | "WARNING" => Some(DMSCLogLevel::Warn),
147 "ERROR" => Some(DMSCLogLevel::Error),
148 _ => None,
149 }
150 })
151 }
152
153 /// Creates a log level from a string.
154 ///
155 /// # Parameters
156 ///
157 /// - `s`: The string to parse
158 ///
159 /// # Returns
160 ///
161 /// An `Option<DMSCLogLevel>` containing the parsed log level
162 pub fn from_str(s: &str) -> Option<Self> {
163 match s.to_ascii_uppercase().as_str() {
164 "DEBUG" => Some(DMSCLogLevel::Debug),
165 "INFO" => Some(DMSCLogLevel::Info),
166 "WARN" | "WARNING" => Some(DMSCLogLevel::Warn),
167 "ERROR" => Some(DMSCLogLevel::Error),
168 _ => None,
169 }
170 }
171}
172
173/// Public logging configuration class.
174///
175/// This struct defines the configuration options for the DMSC logging system, including
176/// log level, output formats, sampling, and log rotation settings.
177#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass(get_all, set_all))]
178#[derive(Clone)]
179pub struct DMSCLogConfig {
180 /// Minimum log level to be logged
181 pub level: DMSCLogLevel,
182 /// Whether console logging is enabled
183 pub console_enabled: bool,
184 /// Whether file logging is enabled
185 pub file_enabled: bool,
186 /// Default sampling rate (0.0 to 1.0, where 1.0 means all logs are sampled)
187 pub sampling_default: f32,
188 /// Name of the log file
189 pub file_name: String,
190 /// Whether to use JSON format for logs
191 pub json_format: bool,
192 /// When to rotate logs (currently only "size" or "none" are supported)
193 pub rotate_when: String,
194 /// Maximum file size in bytes before rotation (used when rotate_when == "size")
195 pub max_bytes: u64,
196 /// Whether to use color blocks in log output
197 pub color_blocks: bool,
198}
199
200#[cfg(feature = "pyo3")]
201#[pyo3::prelude::pymethods]
202impl DMSCLogConfig {
203 #[new]
204 fn py_new() -> Self {
205 Self::default()
206 }
207
208 #[staticmethod]
209 #[pyo3(name = "default")]
210 fn default_py() -> Self {
211 Self::default()
212 }
213
214 #[staticmethod]
215 #[pyo3(signature = (level="info", console_enabled=true, file_enabled=false, file_name="dmsc.log", json_format=false, max_bytes=10485760, color_blocks=true))]
216 fn create(
217 level: &str,
218 console_enabled: bool,
219 file_enabled: bool,
220 file_name: &str,
221 json_format: bool,
222 max_bytes: u64,
223 color_blocks: bool,
224 ) -> Self {
225 let log_level = match level.to_uppercase().as_str() {
226 "DEBUG" => DMSCLogLevel::Debug,
227 "INFO" => DMSCLogLevel::Info,
228 "WARN" | "WARNING" => DMSCLogLevel::Warn,
229 "ERROR" => DMSCLogLevel::Error,
230 _ => DMSCLogLevel::Info,
231 };
232 Self {
233 level: log_level,
234 console_enabled,
235 file_enabled,
236 sampling_default: 1.0,
237 file_name: file_name.to_string(),
238 json_format,
239 rotate_when: "size".to_string(),
240 max_bytes,
241 color_blocks,
242 }
243 }
244}
245
246impl DMSCLogConfig {
247 /// Creates a log configuration from a `DMSCConfig` instance.
248 ///
249 /// This method reads logging configuration from a `DMSCConfig` instance, using the following keys:
250 /// - log.level: Log level (DEBUG, INFO, WARN, ERROR)
251 /// - log.console_enabled: Whether console logging is enabled
252 /// - log.file_enabled: Whether file logging is enabled
253 /// - log.sampling_default: Default sampling rate
254 /// - log.file_name: Name of the log file
255 /// - log.file_format: Log format ("json" for JSON format, anything else for text)
256 /// - log.rotate_when: When to rotate logs
257 /// - log.max_bytes: Maximum file size before rotation
258 ///
259 /// # Parameters
260 ///
261 /// - `config`: The `DMSCConfig` instance to read from
262 ///
263 /// # Returns
264 ///
265 /// A `DMSCLogConfig` instance with configuration from the given `DMSCConfig`
266 pub fn from_config(config: &crate::config::DMSCConfig) -> Self {
267 let mut base = DMSCLogConfig::default();
268
269 if let Some(level_str) = config.get_str("log.level") {
270 if let Some(level) = DMSCLogLevel::from_str(level_str) {
271 base.level = level;
272 }
273 }
274
275 if let Some(v) = config.get_f32("log.sampling_default") {
276 base.sampling_default = v.clamp(0.0, 1.0);
277 }
278
279 if let Some(file_name) = config.get_str("log.file_name") {
280 if !file_name.is_empty() {
281 base.file_name = file_name.to_string();
282 }
283 }
284
285 if let Some(fmt) = config.get_str("log.file_format") {
286 if fmt.eq_ignore_ascii_case("json") {
287 base.json_format = true;
288 }
289 }
290
291 if let Some(rotate) = config.get_str("log.rotate_when") {
292 if !rotate.is_empty() {
293 base.rotate_when = rotate.to_string();
294 }
295 }
296
297 if let Some(v) = config.get_u64("log.max_bytes") {
298 if v > 0 {
299 base.max_bytes = v;
300 }
301 }
302
303 if let Some(v) = config.get_bool("log.console_enabled") {
304 base.console_enabled = v;
305 }
306
307 if let Some(v) = config.get_bool("log.file_enabled") {
308 base.file_enabled = v;
309 }
310
311 if let Some(v) = config.get_bool("log.color_blocks") {
312 base.color_blocks = v;
313 }
314
315 base
316 }
317
318 /// Creates a log configuration from environment variables.
319 ///
320 /// This method reads logging configuration from environment variables:
321 /// - DMSC_LOG_LEVEL: Log level (DEBUG, INFO, WARN, ERROR)
322 /// - DMSC_LOG_CONSOLE_ENABLED: Whether console logging is enabled (true/false)
323 /// - DMSC_LOG_FILE_ENABLED: Whether file logging is enabled (true/false)
324 /// - DMSC_LOG_SAMPLING_DEFAULT: Default sampling rate (0.0-1.0)
325 /// - DMSC_LOG_FILE_NAME: Name of the log file
326 /// - DMSC_LOG_FILE_FORMAT: Log format ("json" for JSON format)
327 /// - DMSC_LOG_ROTATE_WHEN: When to rotate logs
328 /// - DMSC_LOG_MAX_BYTES: Maximum file size before rotation
329 ///
330 /// # Returns
331 ///
332 /// A `DMSCLogConfig` instance with configuration from environment variables
333 pub fn from_env() -> Self {
334 let mut base = DMSCLogConfig::default();
335
336 if let Some(level) = DMSCLogLevel::from_env() {
337 base.level = level;
338 }
339
340 if let Ok(v) = std::env::var("DMSC_LOG_SAMPLING_DEFAULT") {
341 if let Ok(rate) = v.parse::<f32>() {
342 base.sampling_default = rate.clamp(0.0, 1.0);
343 }
344 }
345
346 if let Ok(file_name) = std::env::var("DMSC_LOG_FILE_NAME") {
347 if !file_name.is_empty() {
348 base.file_name = file_name;
349 }
350 }
351
352 if let Ok(fmt) = std::env::var("DMSC_LOG_FILE_FORMAT") {
353 if fmt.eq_ignore_ascii_case("json") {
354 base.json_format = true;
355 }
356 }
357
358 if let Ok(rotate) = std::env::var("DMSC_LOG_ROTATE_WHEN") {
359 if !rotate.is_empty() {
360 base.rotate_when = rotate;
361 }
362 }
363
364 if let Ok(v) = std::env::var("DMSC_LOG_MAX_BYTES") {
365 if let Ok(bytes) = v.parse::<u64>() {
366 if bytes > 0 {
367 base.max_bytes = bytes;
368 }
369 }
370 }
371
372 if let Ok(v) = std::env::var("DMSC_LOG_CONSOLE_ENABLED") {
373 if v.eq_ignore_ascii_case("true") || v == "1" {
374 base.console_enabled = true;
375 } else if v.eq_ignore_ascii_case("false") || v == "0" {
376 base.console_enabled = false;
377 }
378 }
379
380 if let Ok(v) = std::env::var("DMSC_LOG_FILE_ENABLED") {
381 if v.eq_ignore_ascii_case("true") || v == "1" {
382 base.file_enabled = true;
383 } else if v.eq_ignore_ascii_case("false") || v == "0" {
384 base.file_enabled = false;
385 }
386 }
387
388 if let Ok(v) = std::env::var("DMSC_LOG_COLOR_BLOCKS") {
389 if v.eq_ignore_ascii_case("true") || v == "1" {
390 base.color_blocks = true;
391 } else if v.eq_ignore_ascii_case("false") || v == "0" {
392 base.color_blocks = false;
393 }
394 }
395
396 base
397 }
398}
399
400/// Default implementation for DMSCLogConfig
401impl Default for DMSCLogConfig {
402 fn default() -> Self {
403 DMSCLogConfig {
404 level: DMSCLogLevel::Info,
405 console_enabled: true,
406 file_enabled: true,
407 sampling_default: 1.0,
408 file_name: "dms.log".to_string(),
409 json_format: false,
410 rotate_when: "size".to_string(),
411 max_bytes: 10 * 1024 * 1024,
412 color_blocks: true,
413 }
414 }
415}
416
417/// Log entry for caching
418struct LogEntry {
419 level: DMSCLogLevel,
420 target: String,
421 message: String,
422 timestamp: String,
423 context: serde_json::Map<String, serde_json::Value>,
424}
425
426/// Internal logger implementation.
427///
428/// This struct contains the internal implementation of the logging system, including
429/// log level checking, sampling, log message formatting, and caching.
430#[derive(Clone)]
431struct LoggerImpl {
432 /// Minimum log level to be logged
433 level: DMSCLogLevel,
434 /// File system instance for writing log files
435 #[allow(dead_code)]
436 fs: DMSCFileSystem,
437 /// Default sampling rate
438 sampling_default: f32,
439 /// Whether console logging is enabled
440 #[allow(dead_code)]
441 console_enabled: bool,
442 /// Whether file logging is enabled
443 #[allow(dead_code)]
444 file_enabled: bool,
445 /// Name of the log file
446 #[allow(dead_code)]
447 file_name: String,
448 /// Whether to use JSON format for logs
449 #[allow(dead_code)]
450 json_format: bool,
451 /// When to rotate logs (currently only "size" or "none" are supported)
452 #[allow(dead_code)]
453 rotate_when: String,
454 /// Maximum file size in bytes before rotation (used when rotate_when == "size")
455 #[allow(dead_code)]
456 max_bytes: u64,
457 /// Whether to use color blocks in log output
458 #[allow(dead_code)]
459 color_blocks: bool,
460 /// Log cache for batch writing
461 log_cache: Arc<(Mutex<VecDeque<LogEntry>>, Condvar)>,
462 /// Cache size limit
463 cache_size_limit: usize,
464 /// Flush interval in milliseconds
465 #[allow(dead_code)]
466 flush_interval_ms: u64,
467 /// Shutdown flag
468 #[allow(dead_code)]
469 shutdown_flag: Arc<Mutex<bool>>,
470}
471
472impl LoggerImpl {
473 /// Creates a new internal logger implementation.
474 ///
475 /// # Parameters
476 ///
477 /// - `config`: The `DMSCLogConfig` instance to use for configuration
478 /// - `fs`: The `DMSCFileSystem` instance to use for writing log files
479 ///
480 /// # Returns
481 ///
482 /// A new `LoggerImpl` instance
483 fn new(config: &DMSCLogConfig, fs: DMSCFileSystem) -> Self {
484 let log_cache = Arc::new((Mutex::new(VecDeque::new()), Condvar::new()));
485 let shutdown_flag = Arc::new(Mutex::new(false));
486 let cache_size_limit = 1000;
487 let flush_interval_ms = 500;
488
489 // Create a copy of the necessary fields for the background thread
490 let bg_log_cache = Arc::clone(&log_cache);
491 let bg_fs = fs.clone();
492 let bg_file_name = config.file_name.clone();
493 let bg_json_format = config.json_format;
494 let bg_rotate_when = config.rotate_when.clone();
495 let bg_max_bytes = config.max_bytes;
496 let bg_console_enabled = config.console_enabled;
497 let bg_color_blocks = config.color_blocks;
498 let bg_shutdown_flag = Arc::clone(&shutdown_flag);
499
500 // Start background flush thread
501 thread::spawn(move || {
502 let mut last_flush = SystemTime::now();
503
504 loop {
505 // Check if we should shutdown
506 let shutdown_flag = bg_shutdown_flag.lock().expect("Log shutdown flag lock poisoned");
507 if *shutdown_flag {
508 // Flush remaining logs before shutting down
509 Self::flush_cache(
510 &bg_log_cache,
511 &bg_fs,
512 &bg_file_name,
513 bg_json_format,
514 &bg_rotate_when,
515 bg_max_bytes,
516 bg_console_enabled,
517 bg_color_blocks
518 ).unwrap_or(());
519 break;
520 }
521 drop(shutdown_flag);
522
523 // Check if we need to flush based on time or cache size
524 let now = SystemTime::now();
525 let time_since_last_flush = now.duration_since(last_flush).unwrap_or(Duration::from_millis(0));
526
527 let cache_len = bg_log_cache.0.lock().expect("Log cache lock poisoned").len();
528
529 if time_since_last_flush >= Duration::from_millis(flush_interval_ms) || cache_len >= cache_size_limit {
530 Self::flush_cache(
531 &bg_log_cache,
532 &bg_fs,
533 &bg_file_name,
534 bg_json_format,
535 &bg_rotate_when,
536 bg_max_bytes,
537 bg_console_enabled,
538 bg_color_blocks
539 ).unwrap_or(());
540 last_flush = now;
541 }
542
543 // Wait for a short time or until signaled
544 let (lock, cvar) = &*bg_log_cache;
545 let _ = cvar.wait_timeout(lock.lock().unwrap(), Duration::from_millis(100)).unwrap();
546 }
547 });
548
549 LoggerImpl {
550 level: config.level,
551 fs,
552 sampling_default: config.sampling_default,
553 console_enabled: config.console_enabled,
554 file_enabled: config.file_enabled,
555 file_name: config.file_name.clone(),
556 json_format: config.json_format,
557 rotate_when: config.rotate_when.clone(),
558 max_bytes: config.max_bytes,
559 color_blocks: config.color_blocks,
560 log_cache,
561 cache_size_limit,
562 flush_interval_ms,
563 shutdown_flag,
564 }
565 }
566
567 /// Flushes the log cache to disk and console
568 ///
569 /// # Parameters
570 ///
571 /// - `log_cache`: The log cache to flush
572 /// - `fs`: The file system instance to use for writing log files
573 /// - `file_name`: The name of the log file
574 /// - `json_format`: Whether to use JSON format for logs
575 /// - `rotate_when`: When to rotate logs
576 /// - `max_bytes`: Maximum file size before rotation
577 /// - `console_enabled`: Whether console logging is enabled
578 /// - `color_blocks`: Whether to use color blocks in log output
579 ///
580 /// # Returns
581 ///
582 /// A `DMSCResult` indicating success or failure
583 fn flush_cache(
584 log_cache: &Arc<(Mutex<VecDeque<LogEntry>>, Condvar)>,
585 fs: &DMSCFileSystem,
586 file_name: &str,
587 json_format: bool,
588 rotate_when: &str,
589 max_bytes: u64,
590 console_enabled: bool,
591 color_blocks: bool
592 ) -> DMSCResult<()> {
593 let (lock, _cvar) = &**log_cache;
594 let mut cache = lock.lock().unwrap();
595
596 if cache.is_empty() {
597 return Ok(());
598 }
599
600 // Collect all logs to flush
601 let logs_to_flush: Vec<LogEntry> = cache.drain(..).collect();
602 drop(cache);
603
604 // Process logs in batch
605 let mut file_logs = Vec::new();
606 let mut console_logs = Vec::new();
607
608 for log_entry in logs_to_flush {
609 // Format log entry
610 let line = if json_format {
611 // Ensure all required fields are present in JSON format
612 let mut log_obj = log_entry.context.clone();
613
614 // Add any missing standard fields
615 if !log_obj.contains_key("level") {
616 log_obj.insert("level".to_string(), serde_json::Value::String(log_entry.level.as_str().to_string()));
617 }
618 if !log_obj.contains_key("target") {
619 log_obj.insert("target".to_string(), serde_json::Value::String(log_entry.target.clone()));
620 }
621 if !log_obj.contains_key("message") {
622 log_obj.insert("message".to_string(), serde_json::Value::String(log_entry.message.clone()));
623 }
624 if !log_obj.contains_key("timestamp") {
625 log_obj.insert("timestamp".to_string(), serde_json::Value::String(log_entry.timestamp.clone()));
626 }
627
628 serde_json::to_string(&log_obj)?
629 } else {
630 // Extract context fields for text format
631 let ctx_kv: Vec<(String, String)> = log_entry.context.iter()
632 .filter(|(k, _)| *k != "timestamp" && *k != "level" && *k != "target" && *k != "message" && *k != "trace_id" && *k != "span_id")
633 .map(|(k, v)| (k.clone(), v.to_string()))
634 .collect();
635
636 // Extract trace and span IDs if present
637 let trace_info = match (log_entry.context.get("trace_id"), log_entry.context.get("span_id")) {
638 (Some(trace), Some(span)) => format!(" trace_id={trace} span_id={span}"),
639 (Some(trace), None) => format!(" trace_id={trace}"),
640 (None, Some(span)) => format!(" span_id={span}"),
641 (None, None) => String::new(),
642 };
643
644 // Extract event name from context or use target as fallback
645 let event = log_entry.context.get("event")
646 .and_then(|v| v.as_str())
647 .unwrap_or(&log_entry.target);
648
649 // Format context fields for display
650 let ctx_display = if ctx_kv.is_empty() {
651 String::new()
652 } else {
653 let parts: Vec<String> = ctx_kv
654 .iter()
655 .map(|(k, v)| format!("{}={}", k, v))
656 .collect();
657 format!(" | {}", parts.join(", "))
658 };
659
660 // Format trace info for display
661 let trace_display = if trace_info.is_empty() {
662 String::new()
663 } else {
664 format!(" | {}", trace_info.trim())
665 };
666
667 // New log format with optional color block and | separators
668 let color_block = if color_blocks {
669 log_entry.level.color_block()
670 } else {
671 ""
672 };
673 let color_sep = if color_blocks { " | " } else { "" };
674 format!(
675 "{}{}{} | {:5} | {} | event={}{} | {}{}",
676 color_block,
677 color_sep,
678 log_entry.timestamp,
679 log_entry.level.as_str(),
680 log_entry.target,
681 event,
682 trace_display,
683 log_entry.message,
684 ctx_display,
685 )
686 };
687
688 // Separate console and file logs
689 if console_enabled {
690 console_logs.push(line.clone());
691 }
692
693 if !line.is_empty() {
694 file_logs.push(line);
695 }
696 }
697
698 // Write to console in batch
699 if !console_logs.is_empty() {
700 for line in console_logs {
701 log::info!("{line}");
702 }
703 }
704
705 // Write to file in batch
706 if !file_logs.is_empty() {
707 let log_file = fs.logs_dir().join(file_name);
708
709 // Simple size-based rotation if enabled
710 if rotate_when.eq_ignore_ascii_case("size") && max_bytes > 0 {
711 if let Ok(meta) = stdfs::metadata(&log_file) {
712 if meta.len() >= max_bytes {
713 if let Some(parent) = log_file.parent() {
714 let base = log_file.file_name().and_then(|s| s.to_str()).unwrap_or("dms.log");
715 let ts = SystemTime::now()
716 .duration_since(UNIX_EPOCH)
717 .map_err(|e| crate::core::DMSCError::Other(format!("timestamp error: {e}")))?;
718 let rotated = parent.join(format!("{}.{}", base, ts.as_millis()));
719 let _ = stdfs::rename(&log_file, &rotated);
720 }
721 }
722 }
723 }
724
725 // Batch write to file
726 let content = file_logs.join("\n") + "\n";
727 fs.append_text(&log_file, &content)?;
728 }
729
730 Ok(())
731 }
732
733 /// Determines if a message with the given level should be logged.
734 ///
735 /// # Parameters
736 ///
737 /// - `level`: The log level of the message
738 ///
739 /// # Returns
740 ///
741 /// `true` if the message should be logged, `false` otherwise
742 fn should_log(&self, level: DMSCLogLevel) -> bool {
743 (level as u8) >= (self.level as u8)
744 }
745
746 /// Determines if an event should be logged based on sampling.
747 ///
748 /// # Parameters
749 ///
750 /// - `_event`: The event name (currently unused, reserved for future per-event sampling)
751 ///
752 /// # Returns
753 ///
754 /// `true` if the event should be logged, `false` otherwise
755 fn should_log_event(&self, event: &str) -> bool {
756 // Advanced event-based sampling with per-event configuration support
757
758 // First check if we have specific sampling rules for this event
759 if let Some(event_sampling_rate) = self.get_event_sampling_rate(event) {
760 if event_sampling_rate >= 1.0 {
761 return true;
762 } else if event_sampling_rate <= 0.0 {
763 return false;
764 } else {
765 let r = rand::random::<f32>();
766 return r < event_sampling_rate;
767 }
768 }
769
770 // Fall back to default sampling rate
771 if self.sampling_default >= 1.0 {
772 true
773 } else if self.sampling_default <= 0.0 {
774 false
775 } else {
776 let r = rand::random::<f32>();
777 r < self.sampling_default
778 }
779 }
780
781 /// Get the sampling rate for a specific event type
782 ///
783 /// # Parameters
784 ///
785 /// - `event`: The event name to get sampling rate for
786 ///
787 /// # Returns
788 ///
789 /// Optional sampling rate (0.0 to 1.0) if specific rate is configured
790 fn get_event_sampling_rate(&self, event: &str) -> Option<f32> {
791 // In a production environment, this would:
792 // 1. Load per-event sampling configuration from config files
793 // 2. Support dynamic configuration updates
794 // 3. Handle event patterns and categories
795 // 4. Support A/B testing for different sampling rates
796
797 // For now, we support a few common event types with different sampling rates
798 match event {
799 "database_query" => Some(0.1), // Sample 10% of database queries
800 "api_request" => Some(0.5), // Sample 50% of API requests
801 "cache_hit" => Some(0.05), // Sample 5% of cache hits
802 "cache_miss" => Some(1.0), // Log all cache misses
803 "error" => Some(1.0), // Log all errors
804 "warning" => Some(0.8), // Log 80% of warnings
805 _ => None, // Use default for unknown events
806 }
807 }
808
809 /// Returns the current timestamp in ISO 8601 format.
810 ///
811 /// # Returns
812 ///
813 /// A string representing the current timestamp in ISO 8601 format (e.g., "1630000000.123Z")
814 fn now_timestamp() -> String {
815 match SystemTime::now().duration_since(UNIX_EPOCH) {
816 Ok(dur) => {
817 let secs = dur.as_secs();
818 let millis = dur.subsec_millis();
819 format!("{secs}.{millis:03}Z")
820 }
821 Err(_) => "0.000Z".to_string(),
822 }
823 }
824
825 /// Logs a message with the given level, target, and message.
826 ///
827 /// This method handles the complete logging process, including:
828 /// 1. Checking if the message should be logged based on level
829 /// 2. Sampling the message if applicable
830 /// 3. Formatting the message (text or JSON)
831 /// 4. Adding contextual information and distributed tracing fields
832 /// 5. Adding the log entry to the cache for batch processing
833 ///
834 /// # Parameters
835 ///
836 /// - `level`: The log level of the message
837 /// - `target`: The target of the log message (usually a module or component name)
838 /// - `message`: The message to log (must implement `Debug`)
839 ///
840 /// # Returns
841 ///
842 /// A `DMSCResult` indicating success or failure
843 fn log_message<T: Debug>(&self, level: DMSCLogLevel, target: &str, message: T) -> DMSCResult<()> {
844 if !self.should_log(level) {
845 return Ok(());
846 }
847
848 let event = target; // simple default; can be extended to accept explicit event names.
849 if !self.should_log_event(event) {
850 return Ok(());
851 }
852
853 let ts = Self::now_timestamp();
854 let message_str = format!("{message:?}");
855 let ctx_kv = DMSCLogContext::get_all();
856
857 // Create log entry with structured data
858 let mut log_entry_context = serde_json::Map::new();
859 log_entry_context.insert("timestamp".to_string(), json!(ts));
860 log_entry_context.insert("level".to_string(), json!(level.as_str()));
861 log_entry_context.insert("target".to_string(), json!(target));
862 log_entry_context.insert("event".to_string(), json!(event));
863 log_entry_context.insert("message".to_string(), json!(message_str));
864
865 // Add distributed tracing fields if present
866 if let Some(trace_id) = DMSCLogContext::get_trace_id() {
867 log_entry_context.insert("trace_id".to_string(), json!(trace_id));
868 }
869 if let Some(span_id) = DMSCLogContext::get_span_id() {
870 log_entry_context.insert("span_id".to_string(), json!(span_id));
871 }
872 if let Some(parent_span_id) = DMSCLogContext::get_parent_span_id() {
873 log_entry_context.insert("parent_span_id".to_string(), json!(parent_span_id));
874 }
875
876 // Add context fields
877 if !ctx_kv.is_empty() {
878 for (k, v) in ctx_kv.iter() {
879 log_entry_context.insert(k.clone(), json!(v));
880 }
881 }
882
883 // Create log entry for caching
884 let log_entry = LogEntry {
885 level,
886 target: target.to_string(),
887 message: message_str,
888 timestamp: ts,
889 context: log_entry_context,
890 };
891
892 // Add log entry to cache
893 let (lock, cvar) = &*self.log_cache;
894 let mut cache = lock.lock().unwrap();
895 cache.push_back(log_entry);
896
897 // Signal the background thread if cache is full
898 if cache.len() >= self.cache_size_limit {
899 cvar.notify_one();
900 }
901
902 Ok(())
903 }
904}
905
906/// Public-facing logger class.
907///
908/// This struct provides the public API for logging in DMSC, wrapping the internal `LoggerImpl`.
909#[cfg_attr(feature = "pyo3", pyo3::prelude::pyclass)]
910#[derive(Clone)]
911pub struct DMSCLogger {
912 /// Internal logger implementation
913 inner: LoggerImpl,
914}
915
916impl DMSCLogger {
917 /// Creates a new logger instance.
918 ///
919 /// # Parameters
920 ///
921 /// - `config`: The `DMSCLogConfig` instance to use for configuration
922 /// - `fs`: The `DMSCFileSystem` instance to use for writing log files
923 ///
924 /// # Returns
925 ///
926 /// A new `DMSCLogger` instance
927 pub fn new(config: &DMSCLogConfig, fs: DMSCFileSystem) -> Self {
928 let inner = LoggerImpl::new(config, fs);
929 DMSCLogger { inner }
930 }
931
932 /// Logs a debug message.
933 ///
934 /// # Parameters
935 ///
936 /// - `target`: The target of the log message (usually a module or component name)
937 /// - `message`: The message to log (must implement `Debug`)
938 ///
939 /// # Returns
940 ///
941 /// A `DMSCResult` indicating success or failure
942 pub fn debug<T: Debug>(&self, target: &str, message: T) -> DMSCResult<()> {
943 self.inner.log_message(DMSCLogLevel::Debug, target, message)
944 }
945
946 /// Logs an info message.
947 ///
948 /// # Parameters
949 ///
950 /// - `target`: The target of the log message (usually a module or component name)
951 /// - `message`: The message to log (must implement `Debug`)
952 ///
953 /// # Returns
954 ///
955 /// A `DMSCResult` indicating success or failure
956 pub fn info<T: Debug>(&self, target: &str, message: T) -> DMSCResult<()> {
957 self.inner.log_message(DMSCLogLevel::Info, target, message)
958 }
959
960 /// Logs a warning message.
961 ///
962 /// # Parameters
963 ///
964 /// - `target`: The target of the log message (usually a module or component name)
965 /// - `message`: The message to log (must implement `Debug`)
966 ///
967 /// # Returns
968 ///
969 /// A `DMSCResult` indicating success or failure
970 pub fn warn<T: Debug>(&self, target: &str, message: T) -> DMSCResult<()> {
971 self.inner.log_message(DMSCLogLevel::Warn, target, message)
972 }
973
974 /// Logs an error message.
975 ///
976 /// # Parameters
977 ///
978 /// - `target`: The target of the log message (usually a module or component name)
979 /// - `message`: The message to log (must implement `Debug`)
980 ///
981 /// # Returns
982 ///
983 /// A `DMSCResult` indicating success or failure
984 pub fn error<T: Debug>(&self, target: &str, message: T) -> DMSCResult<()> {
985 self.inner.log_message(DMSCLogLevel::Error, target, message)
986 }
987}
988
989#[cfg(feature = "pyo3")]
990#[pyo3::prelude::pymethods]
991impl DMSCLogger {
992 #[new]
993 fn py_new(config: DMSCLogConfig, fs: DMSCFileSystem) -> Self {
994 Self::new(&config, fs)
995 }
996
997 #[pyo3(name = "debug")]
998 fn py_debug(&self, target: &str, message: &str) -> pyo3::PyResult<()> {
999 self.inner
1000 .log_message(DMSCLogLevel::Debug, target, message)
1001 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
1002 }
1003
1004 #[pyo3(name = "info")]
1005 fn py_info(&self, target: &str, message: &str) -> pyo3::PyResult<()> {
1006 self.inner
1007 .log_message(DMSCLogLevel::Info, target, message)
1008 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
1009 }
1010
1011 #[pyo3(name = "warn")]
1012 fn py_warn(&self, target: &str, message: &str) -> pyo3::PyResult<()> {
1013 self.inner
1014 .log_message(DMSCLogLevel::Warn, target, message)
1015 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
1016 }
1017
1018 #[pyo3(name = "error")]
1019 fn py_error(&self, target: &str, message: &str) -> pyo3::PyResult<()> {
1020 self.inner
1021 .log_message(DMSCLogLevel::Error, target, message)
1022 .map_err(|e| pyo3::exceptions::PyRuntimeError::new_err(e.to_string()))
1023 }
1024}