Skip to main content

rusty_tip/
logger.rs

1use log::debug;
2use serde::{de::DeserializeOwned, Serialize};
3use std::{io::Write, path::PathBuf};
4
5use crate::NanonisError;
6
7// Removed LogEntry wrapper - ActionLogEntry already has timestamps
8
9#[derive(Debug)]
10pub struct Logger<T>
11where
12    T: Serialize + Clone + DeserializeOwned,
13{
14    buffer: Vec<T>,
15    buffer_size: usize,
16    file_path: PathBuf,
17    final_format_json: bool, // If true, convert to JSON on final flush
18    flush_failures: usize,
19    max_flush_failures: usize,
20}
21
22impl<T> Logger<T>
23where
24    T: Serialize + Clone + DeserializeOwned,
25{
26    pub fn new<P: Into<PathBuf>>(
27        file_path: P,
28        buffer_size: usize,
29        final_format_json: bool,
30    ) -> Self {
31        let mut path = file_path.into();
32
33        // Automatically add appropriate file extension
34        if final_format_json {
35            // For JSON output, ensure .json extension
36            if path.extension().is_none()
37                || path.extension() != Some(std::ffi::OsStr::new("json"))
38            {
39                path.set_extension("json");
40            }
41        } else {
42            // For JSONL output, ensure .jsonl extension
43            if path.extension().is_none()
44                || path.extension() != Some(std::ffi::OsStr::new("jsonl"))
45            {
46                path.set_extension("jsonl");
47            }
48        }
49
50        Self {
51            buffer: Vec::with_capacity(buffer_size),
52            buffer_size,
53            file_path: path,
54            final_format_json,
55            flush_failures: 0,
56            max_flush_failures: 10,
57        }
58    }
59
60    pub fn add(&mut self, data: T) -> Result<(), NanonisError> {
61        self.buffer.push(data);
62
63        if self.buffer.len() >= self.buffer_size {
64            self.flush()?;
65        }
66
67        Ok(())
68    }
69
70    pub fn flush(&mut self) -> Result<(), NanonisError> {
71        if self.buffer.is_empty() {
72            return Ok(());
73        }
74
75        // Always write JSONL for intermediate flushes (efficient)
76        let file_result = std::fs::OpenOptions::new()
77            .create(true)
78            .append(true)
79            .open(&self.file_path);
80
81        let file = match file_result {
82            Ok(f) => f,
83            Err(e) => {
84                self.flush_failures += 1;
85                log::error!(
86                    "Flush failure {}/{}: Failed to open log file: {}",
87                    self.flush_failures,
88                    self.max_flush_failures,
89                    e
90                );
91
92                // Periodic warning
93                if self.flush_failures > 0 && self.flush_failures % 3 == 0 {
94                    log::warn!(
95                        "Experiencing intermittent flush failures ({}/{})",
96                        self.flush_failures,
97                        self.max_flush_failures
98                    );
99                }
100
101                if self.flush_failures >= self.max_flush_failures {
102                    return Err(NanonisError::Io {
103                        source: e,
104                        context: format!(
105                            "Too many consecutive flush failures ({}) for {:?}",
106                            self.max_flush_failures, self.file_path
107                        ),
108                    });
109                }
110
111                // Don't fail the experiment for transient flush errors
112                return Ok(());
113            }
114        };
115
116        let mut writer = std::io::BufWriter::new(file);
117
118        // Write data
119        let write_result = (|| {
120            for data in &self.buffer {
121                let json_line = serde_json::to_string(data)?;
122                writeln!(writer, "{}", json_line)?;
123            }
124            writer.flush()?;
125            Ok::<(), NanonisError>(())
126        })();
127
128        match write_result {
129            Ok(_) => {
130                self.flush_failures = 0; // Reset on success
131                self.buffer.clear();
132                debug!("Logger flushed successfully to file");
133                Ok(())
134            }
135            Err(e) => {
136                self.flush_failures += 1;
137                log::error!(
138                    "Flush failure {}/{}: Write error: {}",
139                    self.flush_failures,
140                    self.max_flush_failures,
141                    e
142                );
143
144                // Periodic warning
145                if self.flush_failures > 0 && self.flush_failures % 3 == 0 {
146                    log::warn!(
147                        "Experiencing intermittent flush failures ({}/{})",
148                        self.flush_failures,
149                        self.max_flush_failures
150                    );
151                }
152
153                if self.flush_failures >= self.max_flush_failures {
154                    return Err(NanonisError::Io {
155                        source: std::io::Error::other(e.to_string()),
156                        context: format!(
157                            "Too many consecutive flush failures ({}) for {:?}",
158                            self.max_flush_failures, self.file_path
159                        ),
160                    });
161                }
162
163                // Don't fail the experiment for transient flush errors
164                Ok(())
165            }
166        }
167    }
168
169    /// Convert JSONL file to JSON array format (for final post-experiment analysis)
170    pub fn finalize_as_json(&mut self) -> Result<(), NanonisError> {
171        if !self.final_format_json {
172            return Ok(()); // No conversion needed
173        }
174
175        // First flush any remaining buffer
176        self.flush()?;
177
178        // Read all JSONL entries
179        let content =
180            std::fs::read_to_string(&self.file_path).map_err(|source| {
181                NanonisError::Io {
182                    source,
183                    context: format!(
184                        "Could not read JSONL file at {:?}",
185                        self.file_path
186                    ),
187                }
188            })?;
189
190        let mut entries = Vec::new();
191        for line in content.lines() {
192            if !line.trim().is_empty() {
193                let data: T = serde_json::from_str(line)?;
194                entries.push(data);
195            }
196        }
197
198        // Write as JSON array with pretty formatting
199        let json_output = serde_json::to_string_pretty(&entries)?;
200        std::fs::write(&self.file_path, json_output).map_err(|source| {
201            NanonisError::Io {
202                source,
203                context: format!(
204                    "Could not write JSON file at {:?}",
205                    self.file_path
206                ),
207            }
208        })?;
209
210        debug!(
211            "Converted {} entries from JSONL to JSON format",
212            entries.len()
213        );
214        Ok(())
215    }
216
217    pub fn len(&self) -> usize {
218        self.buffer.len()
219    }
220
221    pub fn is_empty(&self) -> bool {
222        self.buffer.len() == 0
223    }
224}
225
226impl<T> Drop for Logger<T>
227where
228    T: Serialize + Clone + DeserializeOwned,
229{
230    fn drop(&mut self) {
231        let _ = self.flush();
232        let _ = self.finalize_as_json();
233    }
234}