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}