dm_database_driver_log/core/
mod.rs1use std::fs::File;
7use std::io::{BufRead, BufReader};
8use std::marker::PhantomData;
9use std::path::{Path, PathBuf};
10
11use crate::encoding::{self, FileEncodingHint};
12use crate::error::ParseError;
13
14#[derive(Copy, Clone, Debug, PartialEq, Eq)]
16pub enum RecordFraming {
17 Line,
19 HeaderDelimited,
21}
22
23pub trait LogRecord {
25 fn method(&self) -> &str;
26 fn category(&self) -> &str;
27 fn used_time_ms(&self) -> Option<f64>;
28 fn exec_id(&self) -> Option<i64>;
29}
30
31pub trait LogFormat: 'static {
36 type Event: LogRecord;
37
38 const FRAMING: RecordFraming;
39
40 fn is_record_start(line: &str) -> bool;
42
43 fn parse_record(record: &str, line_number: u64) -> Result<Self::Event, ParseError>;
47}
48
49#[derive(Debug)]
51pub struct LogParser<F: LogFormat> {
52 path: PathBuf,
53 encoding: FileEncodingHint,
54 _format: PhantomData<F>,
55}
56
57impl<F: LogFormat> LogParser<F> {
58 pub fn iter(&self) -> Result<LogIterator<F>, ParseError> {
60 let file = File::open(&self.path)
61 .map_err(|error| ParseError::IoError(format!("{}: {error}", self.path.display())))?;
62 Ok(LogIterator {
63 reader: BufReader::with_capacity(1 << 20, file),
64 encoding: self.encoding,
65 line_number: 0,
66 line_buf: Vec::with_capacity(4096),
67 lookahead: None,
68 done: false,
69 _format: PhantomData,
70 })
71 }
72
73 pub fn path(&self) -> &Path {
74 &self.path
75 }
76}
77
78pub struct LogParserBuilder<F: LogFormat> {
80 path: PathBuf,
81 encoding: FileEncodingHint,
82 _format: PhantomData<F>,
83}
84
85impl<F: LogFormat> LogParserBuilder<F> {
86 pub fn new<P: AsRef<Path>>(path: P) -> Self {
87 Self {
88 path: path.as_ref().to_path_buf(),
89 encoding: FileEncodingHint::Auto,
90 _format: PhantomData,
91 }
92 }
93
94 pub fn encoding_hint(mut self, hint: FileEncodingHint) -> Self {
96 self.encoding = hint;
97 self
98 }
99
100 pub fn build(self) -> Result<LogParser<F>, ParseError> {
102 let path = self.path;
103 match File::open(&path) {
104 Ok(_) => Ok(LogParser {
105 path,
106 encoding: self.encoding,
107 _format: PhantomData,
108 }),
109 Err(error) => Err(ParseError::IoError(format!("{}: {error}", path.display()))),
110 }
111 }
112}
113
114pub struct LogIterator<F: LogFormat> {
116 reader: BufReader<File>,
117 encoding: FileEncodingHint,
118 line_number: u64,
119 line_buf: Vec<u8>,
120 lookahead: Option<(u64, String)>,
121 done: bool,
122 _format: PhantomData<F>,
123}
124
125impl<F: LogFormat> LogIterator<F> {
126 pub fn skip_errors(self) -> impl Iterator<Item = F::Event> {
128 self.filter_map(Result::ok)
129 }
130
131 pub fn filter_by_method(
133 self,
134 method: &str,
135 ) -> impl Iterator<Item = Result<F::Event, ParseError>> + '_ {
136 self.filter(move |result| match result {
137 Ok(event) => event.method() == method,
138 Err(_) => true,
139 })
140 }
141
142 pub fn filter_by_category(
144 self,
145 category: &str,
146 ) -> impl Iterator<Item = Result<F::Event, ParseError>> + '_ {
147 self.filter(move |result| match result {
148 Ok(event) => event.category() == category,
149 Err(_) => true,
150 })
151 }
152
153 pub fn filter_by_used_time(
155 self,
156 min_ms: f64,
157 ) -> impl Iterator<Item = Result<F::Event, ParseError>> {
158 self.filter(move |result| match result {
159 Ok(event) => event.used_time_ms().is_some_and(|value| value >= min_ms),
160 Err(_) => true,
161 })
162 }
163
164 pub fn filter_by_exec_id(
166 self,
167 exec_id: i64,
168 ) -> impl Iterator<Item = Result<F::Event, ParseError>> {
169 self.filter(move |result| match result {
170 Ok(event) => event.exec_id() == Some(exec_id),
171 Err(_) => true,
172 })
173 }
174
175 fn read_line(&mut self) -> Result<Option<(u64, String)>, ParseError> {
176 self.line_buf.clear();
177 let bytes_read = self
178 .reader
179 .read_until(b'\n', &mut self.line_buf)
180 .map_err(|error| ParseError::IoError(error.to_string()))?;
181 if bytes_read == 0 {
182 return Ok(None);
183 }
184
185 self.line_number += 1;
186 let mut end = self.line_buf.len();
187 while end > 0 && matches!(self.line_buf[end - 1], b'\n' | b'\r') {
188 end -= 1;
189 }
190 Ok(Some((
191 self.line_number,
192 encoding::decode(&self.line_buf[..end], self.encoding),
193 )))
194 }
195
196 fn parse_record(&self, record: &str, line_number: u64) -> Result<F::Event, ParseError> {
197 F::parse_record(record, line_number)
198 .map_err(|error| error.with_context(record, line_number))
199 }
200
201 fn next_line_record(&mut self) -> Option<Result<F::Event, ParseError>> {
202 loop {
203 let (line_number, line) = match self.read_line() {
204 Ok(Some(line)) => line,
205 Ok(None) => {
206 self.done = true;
207 return None;
208 }
209 Err(error) => {
210 self.done = true;
211 return Some(Err(error));
212 }
213 };
214 if line.is_empty() {
215 continue;
216 }
217 return Some(self.parse_record(&line, line_number));
218 }
219 }
220
221 fn next_header_delimited_record(&mut self) -> Option<Result<F::Event, ParseError>> {
222 let mut record = String::new();
223 let mut start_line = 0;
224
225 loop {
226 let next_line = match self.lookahead.take() {
227 Some(line) => Some(line),
228 None => match self.read_line() {
229 Ok(line) => line,
230 Err(error) => {
231 self.done = true;
232 return Some(Err(error));
233 }
234 },
235 };
236 let Some((line_number, line)) = next_line else {
237 self.done = true;
238 if record.is_empty() {
239 return None;
240 }
241 break;
242 };
243
244 if F::is_record_start(&line) && !record.is_empty() {
245 self.lookahead = Some((line_number, line));
246 break;
247 }
248 if record.is_empty() {
249 if line.is_empty() {
250 continue;
251 }
252 start_line = line_number;
253 record = line;
254 } else {
255 record.push('\n');
256 record.push_str(&line);
257 }
258 }
259
260 Some(self.parse_record(&record, start_line))
261 }
262}
263
264impl<F: LogFormat> Iterator for LogIterator<F> {
265 type Item = Result<F::Event, ParseError>;
266
267 fn next(&mut self) -> Option<Self::Item> {
268 if self.done {
269 return None;
270 }
271 match F::FRAMING {
272 RecordFraming::Line => self.next_line_record(),
273 RecordFraming::HeaderDelimited => self.next_header_delimited_record(),
274 }
275 }
276}