iflow_cli_sdk_rust/
logger.rs

1//! Logger module for recording iFlow messages
2//!
3//! This module provides functionality for logging messages exchanged with iFlow
4//! to files, with support for log rotation based on file size.
5
6use crate::Message;
7use std::fs::{File, OpenOptions};
8use std::io::{self, BufWriter, Write};
9use std::path::{Path, PathBuf};
10use std::sync::Arc;
11use tokio::sync::Mutex;
12
13/// Logger configuration
14///
15/// Configuration options for the message logger, including file paths,
16/// size limits, and retention policies.
17#[derive(Debug, Clone)]
18pub struct LoggerConfig {
19    /// Log file path
20    pub log_file: PathBuf,
21    /// Whether to enable logging
22    pub enabled: bool,
23    /// Maximum log file size (bytes), will rotate when exceeded
24    pub max_file_size: u64,
25    /// Number of log files to retain
26    pub max_files: u32,
27}
28
29impl Default for LoggerConfig {
30    fn default() -> Self {
31        Self {
32            log_file: PathBuf::from("iflow_messages.log"),
33            enabled: true,
34            max_file_size: 10 * 1024 * 1024, // 10MB
35            max_files: 5,
36        }
37    }
38}
39
40/// Message logger
41///
42/// Handles writing iFlow messages to log files with automatic rotation
43/// based on file size limits.
44#[derive(Clone)]
45pub struct MessageLogger {
46    config: LoggerConfig,
47    writer: Arc<Mutex<BufWriter<File>>>,
48}
49
50impl MessageLogger {
51    /// Create a new logger
52    ///
53    /// Creates a new message logger with the specified configuration.
54    /// If logging is disabled, it will create a null writer that discards all output.
55    ///
56    /// # Arguments
57    /// * `config` - The logger configuration
58    ///
59    /// # Returns
60    /// * `Ok(MessageLogger)` if the logger was created successfully
61    /// * `Err(io::Error)` if there was an error creating the log file
62    pub fn new(config: LoggerConfig) -> Result<Self, io::Error> {
63        if !config.enabled {
64            return Ok(Self {
65                config,
66                writer: Arc::new(Mutex::new(BufWriter::new(File::create("/dev/null")?))),
67            });
68        }
69
70        // Check if log file rotation is needed
71        if let Some(parent) = config.log_file.parent() {
72            std::fs::create_dir_all(parent)?;
73        }
74
75        // Check file size
76        if config.log_file.exists() {
77            let metadata = std::fs::metadata(&config.log_file)?;
78            if metadata.len() >= config.max_file_size {
79                Self::rotate_log_file(&config)?;
80            }
81        }
82
83        let file = OpenOptions::new()
84            .create(true)
85            .append(true)
86            .open(&config.log_file)?;
87
88        Ok(Self {
89            config,
90            writer: Arc::new(Mutex::new(BufWriter::new(file))),
91        })
92    }
93
94    /// Rotate log files
95    ///
96    /// Rotates the log files based on the configured retention policy.
97    /// This method manages the renaming and deletion of old log files.
98    ///
99    /// # Arguments
100    /// * `config` - The logger configuration containing rotation settings
101    ///
102    /// # Returns
103    /// * `Ok(())` if the rotation was successful
104    /// * `Err(io::Error)` if there was an error during rotation
105    fn rotate_log_file(config: &LoggerConfig) -> Result<(), io::Error> {
106        if !config.log_file.exists() {
107            return Ok(());
108        }
109
110        // Delete the oldest log file
111        for i in (0..config.max_files).rev() {
112            let old_path = if i == 0 {
113                config.log_file.clone()
114            } else {
115                config.log_file.with_extension(format!("log.{}", i))
116            };
117
118            let new_path = if i + 1 >= config.max_files {
119                // Delete files that exceed the retention count
120                if old_path.exists() {
121                    std::fs::remove_file(&old_path)?;
122                }
123                continue;
124            } else {
125                config.log_file.with_extension(format!("log.{}", i + 1))
126            };
127
128            if old_path.exists() {
129                std::fs::rename(&old_path, &new_path)?;
130            }
131        }
132
133        Ok(())
134    }
135
136    /// Log a message
137    ///
138    /// Writes a message to the log file, handling file rotation if necessary.
139    /// This method is async and thread-safe.
140    ///
141    /// # Arguments
142    /// * `message` - The message to log
143    ///
144    /// # Returns
145    /// * `Ok(())` if the message was logged successfully
146    /// * `Err(io::Error)` if there was an error writing to the log file
147    pub async fn log_message(&self, message: &Message) -> Result<(), io::Error> {
148        if !self.config.enabled {
149            return Ok(());
150        }
151
152        let log_entry = self.format_message(message);
153        let mut writer = self.writer.lock().await;
154
155        writeln!(writer, "{}", log_entry)?;
156        writer.flush()?;
157
158        // Check file size
159        if writer.get_ref().metadata()?.len() >= self.config.max_file_size {
160            Self::rotate_log_file(&self.config)?;
161
162            // Reopen the file
163            let file = OpenOptions::new()
164                .create(true)
165                .append(true)
166                .open(&self.config.log_file)?;
167            *writer = BufWriter::new(file);
168        }
169
170        Ok(())
171    }
172
173    /// Log raw message using Debug format without any processing
174    ///
175    /// Formats a message for logging using the Debug trait.
176    /// This provides a detailed representation of the message structure.
177    ///
178    /// # Arguments
179    /// * `message` - The message to format
180    ///
181    /// # Returns
182    /// A formatted string representation of the message
183    fn format_message(&self, message: &Message) -> String {
184        // Output raw message structure using Debug format
185        // Use alternate format to avoid truncation
186        format!("{:#?}", message)
187    }
188
189    /// Get current log file path
190    ///
191    /// Returns the path to the current log file.
192    ///
193    /// # Returns
194    /// A reference to the log file path
195    pub fn log_file_path(&self) -> &Path {
196        &self.config.log_file
197    }
198
199    /// Get configuration
200    ///
201    /// Returns the logger configuration.
202    ///
203    /// # Returns
204    /// A reference to the logger configuration
205    pub fn config(&self) -> &LoggerConfig {
206        &self.config
207    }
208}