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