1use std::io::Write;
4use std::path::PathBuf;
5
6use crate::error::Result;
7use crate::session_id::SessionId;
8
9pub trait Log: Send {
10 fn on_incoming(&mut self, raw: &[u8]);
11 fn on_outgoing(&mut self, raw: &[u8]);
12 fn on_event(&mut self, event: &str);
13}
14
15pub trait LogFactory: Send + Sync {
16 fn create(&self, session_id: &SessionId) -> Result<Box<dyn Log>>;
17}
18
19#[derive(Debug, Default)]
22pub struct NullLog;
23
24impl Log for NullLog {
25 fn on_incoming(&mut self, _raw: &[u8]) {}
26 fn on_outgoing(&mut self, _raw: &[u8]) {}
27 fn on_event(&mut self, _event: &str) {}
28}
29
30#[derive(Debug, Default)]
31pub struct NullLogFactory;
32
33impl LogFactory for NullLogFactory {
34 fn create(&self, _session_id: &SessionId) -> Result<Box<dyn Log>> {
35 Ok(Box::new(NullLog))
36 }
37}
38
39pub struct TracingLog {
44 session_id: String,
45}
46
47fn printable(raw: &[u8]) -> String {
48 String::from_utf8_lossy(raw).replace('\x01', "|")
49}
50
51impl Log for TracingLog {
52 fn on_incoming(&mut self, raw: &[u8]) {
53 tracing::debug!(session = %self.session_id, "<- {}", printable(raw));
54 }
55 fn on_outgoing(&mut self, raw: &[u8]) {
56 tracing::debug!(session = %self.session_id, "-> {}", printable(raw));
57 }
58 fn on_event(&mut self, event: &str) {
59 tracing::info!(session = %self.session_id, "{event}");
60 }
61}
62
63#[derive(Debug, Default)]
64pub struct TracingLogFactory;
65
66impl LogFactory for TracingLogFactory {
67 fn create(&self, session_id: &SessionId) -> Result<Box<dyn Log>> {
68 Ok(Box::new(TracingLog { session_id: session_id.to_string() }))
69 }
70}
71
72#[derive(Debug, Clone, Copy)]
79pub struct Rotation {
80 pub max_size: u64,
81 pub max_backups: usize,
82}
83
84impl Rotation {
85 fn none() -> Self {
86 Self { max_size: 0, max_backups: 0 }
87 }
88}
89
90struct RotatingFile {
92 path: PathBuf,
93 file: std::fs::File,
94 size: u64,
95 rotation: Rotation,
96}
97
98impl RotatingFile {
99 fn open(path: PathBuf, rotation: Rotation) -> Result<Self> {
100 let file =
101 std::fs::OpenOptions::new().create(true).append(true).open(&path)?;
102 let size = file.metadata().map(|m| m.len()).unwrap_or(0);
103 Ok(Self { path, file, size, rotation })
104 }
105
106 fn write_line(&mut self, line: &str) {
107 let bytes = line.len() as u64 + 1; if self.rotation.max_size > 0
109 && self.size > 0
110 && self.size + bytes > self.rotation.max_size
111 {
112 if let Err(e) = self.rotate() {
113 tracing::warn!("log rotation failed for {:?}: {e}", self.path);
114 }
115 }
116 if writeln!(self.file, "{line}").is_ok() {
117 self.size += bytes;
118 }
119 }
120
121 fn rotate(&mut self) -> std::io::Result<()> {
122 let backup = |n: usize| -> PathBuf {
123 let mut p = self.path.clone().into_os_string();
124 p.push(format!(".{n}"));
125 PathBuf::from(p)
126 };
127 let _ = std::fs::remove_file(backup(self.rotation.max_backups));
129 for n in (1..self.rotation.max_backups).rev() {
130 if backup(n).exists() {
131 std::fs::rename(backup(n), backup(n + 1))?;
132 }
133 }
134 if self.rotation.max_backups > 0 {
135 std::fs::rename(&self.path, backup(1))?;
136 } else {
137 std::fs::remove_file(&self.path)?;
139 }
140 self.file =
141 std::fs::OpenOptions::new().create(true).append(true).open(&self.path)?;
142 self.size = 0;
143 Ok(())
144 }
145}
146
147pub struct FileLog {
150 messages: RotatingFile,
151 events: RotatingFile,
152}
153
154impl Log for FileLog {
155 fn on_incoming(&mut self, raw: &[u8]) {
156 self.messages.write_line(&format!("{} <- {}", now(), printable(raw)));
157 }
158 fn on_outgoing(&mut self, raw: &[u8]) {
159 self.messages.write_line(&format!("{} -> {}", now(), printable(raw)));
160 }
161 fn on_event(&mut self, event: &str) {
162 self.events.write_line(&format!("{} {}", now(), event));
163 }
164}
165
166fn now() -> String {
167 chrono::Utc::now().format("%Y%m%d-%H:%M:%S%.3f").to_string()
168}
169
170pub struct FileLogFactory {
171 pub path: PathBuf,
172 rotation: Rotation,
173}
174
175impl FileLogFactory {
176 pub fn new(path: impl Into<PathBuf>) -> Self {
177 Self { path: path.into(), rotation: Rotation::none() }
178 }
179
180 pub fn with_rotation(mut self, max_size: u64, max_backups: usize) -> Self {
183 self.rotation = Rotation { max_size, max_backups };
184 self
185 }
186}
187
188impl LogFactory for FileLogFactory {
189 fn create(&self, session_id: &SessionId) -> Result<Box<dyn Log>> {
190 std::fs::create_dir_all(&self.path)?;
191 let prefix = self.path.join(session_id.file_prefix());
192 Ok(Box::new(FileLog {
193 messages: RotatingFile::open(prefix.with_extension("messages.log"), self.rotation)?,
194 events: RotatingFile::open(prefix.with_extension("event.log"), self.rotation)?,
195 }))
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn file_log_rotates_and_prunes_backups() {
205 let dir = std::env::temp_dir().join(format!("qft-log-{}", std::process::id()));
206 let _ = std::fs::remove_dir_all(&dir);
207 let sid = SessionId::new("FIX.4.4", "S", "T");
208 let factory = FileLogFactory::new(&dir).with_rotation(80, 2);
210 let mut log = factory.create(&sid).unwrap();
211
212 for i in 0..20 {
214 log.on_outgoing(format!("8=FIX.4.4|35=0|34={i}|line-padding-here|").as_bytes());
215 }
216 drop(log);
217
218 let base = dir.join(sid.file_prefix()).with_extension("messages.log");
219 assert!(base.exists(), "current log missing");
220 let b1 = with_suffix(&base, ".1");
221 let b2 = with_suffix(&base, ".2");
222 let b3 = with_suffix(&base, ".3");
223 assert!(b1.exists() && b2.exists(), "expected 2 rotated backups");
224 assert!(!b3.exists(), "backups exceeded max_backups=2");
226 let _ = std::fs::remove_dir_all(&dir);
227 }
228
229 fn with_suffix(p: &std::path::Path, s: &str) -> PathBuf {
230 let mut o = p.to_path_buf().into_os_string();
231 o.push(s);
232 PathBuf::from(o)
233 }
234}