1use itertools::join;
10use std::fmt;
11use std::io::BufRead;
12use std::io::BufReader;
13use std::io::Lines;
14use std::io::Read;
15use std::str;
16use std::str::FromStr;
17
18use gst::{ClockTime, DebugLevel, Structure};
19use gstreamer as gst;
20use lazy_static::lazy_static;
21use regex::Regex;
22use thiserror::Error;
23
24#[derive(Debug, PartialEq)]
25pub enum TimestampField {
26 Hour,
27 Minute,
28 Second,
29 SubSecond,
30}
31
32#[derive(Debug, PartialEq)]
33pub enum Token {
34 Timestamp { field: Option<TimestampField> },
35 PID,
36 Thread,
37 Level,
38 Category,
39 File,
40 LineNumber,
41 Function,
42 Message,
43 Object,
44}
45
46#[derive(Debug, Error, PartialEq)]
47pub enum ParsingError {
48 #[error("invalid debug level: {}", name)]
49 InvalidDebugLevel { name: String },
50 #[error("invalid timestamp: {} : {:?}", ts, field)]
51 InvalidTimestamp { ts: String, field: TimestampField },
52 #[error("missing token: {:?}", t)]
53 MissingToken { t: Token },
54 #[error("invalid PID: {}", pid)]
55 InvalidPID { pid: String },
56 #[error("missing location")]
57 MissingLocation,
58 #[error("invalid line number: {}", line)]
59 InvalidLineNumber { line: String },
60}
61
62#[derive(Debug)]
63pub struct Entry {
64 pub ts: ClockTime,
65 pub pid: u32,
66 pub thread: String,
67 pub level: DebugLevel,
68 pub category: String,
69 pub file: String,
70 pub line: u32,
71 pub function: String,
72 pub message: String,
73 pub object: Option<String>,
74}
75
76fn parse_debug_level(s: &str) -> Result<DebugLevel, ParsingError> {
77 match s {
78 "ERROR" => Ok(DebugLevel::Error),
79 "WARN" => Ok(DebugLevel::Warning),
80 "FIXME" => Ok(DebugLevel::Fixme),
81 "INFO" => Ok(DebugLevel::Info),
82 "DEBUG" => Ok(DebugLevel::Debug),
83 "LOG" => Ok(DebugLevel::Log),
84 "TRACE" => Ok(DebugLevel::Trace),
85 "MEMDUMP" => Ok(DebugLevel::Memdump),
86 _ => Err(ParsingError::InvalidDebugLevel {
87 name: s.to_string(),
88 }),
89 }
90}
91
92fn parse_time(ts: &str) -> Result<ClockTime, ParsingError> {
93 let mut split = ts.splitn(3, ':');
94 let h: u64 = split
95 .next()
96 .ok_or(ParsingError::MissingToken {
97 t: Token::Timestamp {
98 field: Some(TimestampField::Hour),
99 },
100 })?
101 .parse()
102 .map_err(|_e| ParsingError::InvalidTimestamp {
103 ts: ts.to_string(),
104 field: TimestampField::Hour,
105 })?;
106
107 let m: u64 = split
108 .next()
109 .ok_or(ParsingError::MissingToken {
110 t: Token::Timestamp {
111 field: Some(TimestampField::Minute),
112 },
113 })?
114 .parse()
115 .map_err(|_e| ParsingError::InvalidTimestamp {
116 ts: ts.to_string(),
117 field: TimestampField::Minute,
118 })?;
119
120 split = split
121 .next()
122 .ok_or(ParsingError::MissingToken {
123 t: Token::Timestamp {
124 field: Some(TimestampField::Second),
125 },
126 })?
127 .splitn(2, '.');
128 let secs: u64 = split
129 .next()
130 .ok_or(ParsingError::MissingToken {
131 t: Token::Timestamp {
132 field: Some(TimestampField::Second),
133 },
134 })?
135 .parse()
136 .map_err(|_e| ParsingError::InvalidTimestamp {
137 ts: ts.to_string(),
138 field: TimestampField::Second,
139 })?;
140
141 let subsecs: u64 = split
142 .next()
143 .ok_or(ParsingError::MissingToken {
144 t: Token::Timestamp {
145 field: Some(TimestampField::SubSecond),
146 },
147 })?
148 .parse()
149 .map_err(|_e| ParsingError::InvalidTimestamp {
150 ts: ts.to_string(),
151 field: TimestampField::SubSecond,
152 })?;
153
154 Ok(ClockTime::from_seconds(h * 60 * 60 + m * 60 + secs) + ClockTime::from_nseconds(subsecs))
155}
156
157fn split_location(location: &str) -> Result<(String, u32, String, Option<String>), ParsingError> {
158 let mut split = location.splitn(4, ':');
159 let file = split
160 .next()
161 .ok_or(ParsingError::MissingToken { t: Token::File })?;
162 let line_str = split.next().ok_or(ParsingError::MissingToken {
163 t: Token::LineNumber,
164 })?;
165 let line = line_str
166 .parse()
167 .map_err(|_e| ParsingError::InvalidLineNumber {
168 line: line_str.to_string(),
169 })?;
170
171 let function = split
172 .next()
173 .ok_or(ParsingError::MissingToken { t: Token::Function })?;
174
175 let object = split
176 .next()
177 .ok_or(ParsingError::MissingToken { t: Token::Object })?;
178
179 let object_name = {
180 if !object.is_empty() {
181 let object = object
182 .to_string()
183 .trim_start_matches('<')
184 .trim_end_matches('>')
185 .to_string();
186
187 Some(object)
188 } else {
189 None
190 }
191 };
192
193 Ok((file.to_string(), line, function.to_string(), object_name))
194}
195
196impl Entry {
197 fn new(line: &str) -> Result<Entry, ParsingError> {
198 lazy_static! {
200 static ref RE: Regex = Regex::new("\x1b\\[[0-9;]*m").unwrap();
201 }
202 let line = RE.replace_all(line, "");
203
204 let mut it = line.split(' ');
205 let ts_str = it.next().ok_or(ParsingError::MissingToken {
206 t: Token::Timestamp { field: None },
207 })?;
208 let ts = parse_time(ts_str)?;
209
210 let mut it = it.skip_while(|x| x.is_empty());
211 let pid_str = it
212 .next()
213 .ok_or(ParsingError::MissingToken { t: Token::PID })?;
214 let pid = pid_str.parse().map_err(|_e| ParsingError::InvalidPID {
215 pid: pid_str.to_string(),
216 })?;
217
218 let mut it = it.skip_while(|x| x.is_empty());
219 let thread = it
220 .next()
221 .ok_or(ParsingError::MissingToken { t: Token::Thread })?
222 .to_string();
223
224 let mut it = it.skip_while(|x| x.is_empty());
225 let level_str = it
226 .next()
227 .ok_or(ParsingError::MissingToken { t: Token::Level })?;
228 let level = parse_debug_level(level_str)?;
229
230 let mut it = it.skip_while(|x| x.is_empty());
231 let category = it
232 .next()
233 .ok_or(ParsingError::MissingToken { t: Token::Category })?
234 .to_string();
235
236 let mut it = it.skip_while(|x| x.is_empty());
237 let location_str = it.next().ok_or(ParsingError::MissingLocation)?;
238 let (file, line, function, object) = split_location(location_str)?;
239 let message: String = join(it, " ");
240
241 Ok(Entry {
242 ts,
243 pid,
244 thread,
245 level,
246 category,
247 file,
248 line,
249 function,
250 object,
251 message,
252 })
253 }
254
255 pub fn message_to_struct(&self) -> Option<Structure> {
256 Structure::from_str(&self.message).ok()
257 }
258}
259
260impl fmt::Display for Entry {
261 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
262 write!(
263 f,
264 "{} {} {} {:?} {} {}:{}:{}:<{}> {}",
265 self.ts,
266 self.pid,
267 self.thread,
268 self.level,
269 self.category,
270 self.file,
271 self.line,
272 self.function,
273 self.object.clone().unwrap_or_default(),
274 self.message
275 )
276 }
277}
278
279pub struct ParserIterator<R: Read> {
280 lines: Lines<BufReader<R>>,
281}
282
283impl<R: Read> ParserIterator<R> {
284 fn new(lines: Lines<BufReader<R>>) -> Self {
285 Self { lines }
286 }
287}
288
289impl<R: Read> Iterator for ParserIterator<R> {
290 type Item = Entry;
291
292 fn next(&mut self) -> Option<Entry> {
293 match self.lines.next() {
294 None => None,
295 Some(line) => match Entry::new(&line.unwrap()) {
296 Ok(entry) => Some(entry),
297 Err(_err) => self.next(),
298 },
299 }
300 }
301}
302
303pub fn parse<R: Read>(r: R) -> ParserIterator<R> {
304 gst::init().expect("Failed to initialize gst");
305
306 let file = BufReader::new(r);
307
308 ParserIterator::new(file.lines())
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use std::fs::File;
315
316 #[test]
317 fn no_color() {
318 let f = File::open("test-logs/nocolor.log").expect("Failed to open log file");
319 let mut parsed = parse(f);
320
321 let entry = parsed.next().expect("First entry missing");
322 assert_eq!(entry.ts.nseconds(), 7773544);
323 assert_eq!(format!("{}", entry.ts), "0:00:00.007773544");
324 assert_eq!(entry.pid, 8874);
325 assert_eq!(entry.thread, "0x558951015c00");
326 assert_eq!(entry.level, DebugLevel::Info);
327 assert_eq!(entry.category, "GST_INIT");
328 assert_eq!(entry.file, "gst.c");
329 assert_eq!(entry.line, 510);
330 assert_eq!(entry.function, "init_pre");
331 assert_eq!(
332 entry.message,
333 "Initializing GStreamer Core Library version 1.10.4"
334 );
335
336 let entry = parsed.nth(3).expect("3th entry missing");
337 assert_eq!(entry.message, "0x55895101d040 ref 1->2");
338 assert_eq!(entry.object, Some("allocatorsysmem0".to_string()));
339 }
340
341 fn parse_file(f: File) -> (Entry, usize) {
342 let mut parsed = parse(f);
343
344 let entry = parsed.next().expect("First entry missing");
345 (entry, parsed.count() + 1)
346 }
347
348 #[test]
349 fn color() {
350 let f = File::open("test-logs/color.log").expect("Failed to open log file");
351 let (entry, count) = parse_file(f);
352 assert_eq!(entry.ts.nseconds(), 208614);
353 assert_eq!(format!("{}", entry.ts), "0:00:00.000208614");
354 assert_eq!(entry.pid, 17267);
355 assert_eq!(entry.thread, "0x2192200");
356 assert_eq!(entry.level, DebugLevel::Info);
357 assert_eq!(entry.category, "GST_INIT");
358 assert_eq!(entry.file, "gst.c");
359 assert_eq!(entry.line, 584);
360 assert_eq!(entry.function, "init_pre");
361 assert_eq!(
362 entry.message,
363 "Initializing GStreamer Core Library version 1.13.0.1"
364 );
365 assert_eq!(count, 15);
366 }
367
368 #[test]
369 fn corrupted() {
370 let f = File::open("test-logs/corrupted-nocolor.log").expect("Failed to open log file");
371 let (entry, count) = parse_file(f);
372
373 assert_eq!(entry.ts.nseconds(), 7773544);
374 assert_eq!(format!("{}", entry.ts), "0:00:00.007773544");
375 assert_eq!(entry.pid, 8874);
376 assert_eq!(entry.thread, "0x558951015c00");
377 assert_eq!(entry.level, DebugLevel::Info);
378 assert_eq!(entry.category, "GST_INIT");
379 assert_eq!(entry.file, "gst.c");
380 assert_eq!(entry.line, 510);
381 assert_eq!(entry.function, "init_pre");
382 assert_eq!(
383 entry.message,
384 "Initializing GStreamer Core Library version 1.10.4"
385 );
386 assert_eq!(count, 6);
387 }
388
389 #[test]
390 fn parse_corrupted() {
391 let f = File::open("test-logs/corrupted-nocolor.log").expect("Failed to open log file");
392 let (_entry, count) = parse_file(f);
393 assert!(count > 0);
394 }
395
396 #[test]
397 fn timestamps() {
398 assert!(Entry::new("foo").is_err());
399
400 let e1 = "e:00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c:510:init_pre: Init";
401 match Entry::new(e1) {
402 Ok(_) => unreachable!(),
403 Err(e) => assert_eq!(
404 e,
405 ParsingError::InvalidTimestamp {
406 ts: "e:00:00.007773544".to_string(),
407 field: TimestampField::Hour,
408 }
409 ),
410 };
411
412 let e1 = ":00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c:510:init_pre: Init";
413 match Entry::new(e1) {
414 Ok(_) => unreachable!(),
415 Err(e) => assert_eq!(
416 e,
417 ParsingError::InvalidTimestamp {
418 ts: ":00:00.007773544".to_string(),
419 field: TimestampField::Hour,
420 }
421 ),
422 };
423
424 let e1 = "8874 0x558951015c00 INFO GST_INIT gst.c:510:init_pre: Init";
425 match Entry::new(e1) {
426 Ok(_) => unreachable!(),
427 Err(e) => assert_eq!(
428 e,
429 ParsingError::MissingToken {
430 t: Token::Timestamp {
431 field: Some(TimestampField::Minute)
432 },
433 }
434 ),
435 };
436 }
437
438 #[test]
439 fn pid() {
440 let e1 = "00:00:00.007773544 ";
441 match Entry::new(e1) {
442 Ok(_) => unreachable!(),
443 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::PID }),
444 };
445
446 let e1 = "00:00:00.007773544 8fuz874 0x558951015c00 INFO GST_INIT gst.c:510:init_pre: Init";
447 match Entry::new(e1) {
448 Ok(_) => unreachable!(),
449 Err(e) => assert_eq!(
450 e,
451 ParsingError::InvalidPID {
452 pid: "8fuz874".to_string(),
453 }
454 ),
455 };
456 }
457
458 #[test]
459 fn thread() {
460 let e1 = "00:00:00.007773544 8874 ";
461 match Entry::new(e1) {
462 Ok(_) => unreachable!(),
463 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::Thread }),
464 };
465 }
466
467 #[test]
468 fn debug_level() {
469 let e1 = "00:00:00.007773544 8874 0x558951015c00 ";
470 match Entry::new(e1) {
471 Ok(_) => unreachable!(),
472 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::Level }),
473 };
474
475 let e1 = "00:00:00.007773544 8874 0x558951015c00 FUZZLEVEL GST_INIT gst.c:510:init_pre: Init";
476 match Entry::new(e1) {
477 Ok(_) => unreachable!(),
478 Err(e) => assert_eq!(
479 e,
480 ParsingError::InvalidDebugLevel {
481 name: "FUZZLEVEL".to_string(),
482 }
483 ),
484 };
485 }
486
487 #[test]
488 fn category() {
489 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO";
490 match Entry::new(e1) {
491 Ok(_) => unreachable!(),
492 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::Category }),
493 };
494 }
495
496 #[test]
497 fn location() {
498 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO GST_INIT";
499 match Entry::new(e1) {
500 Ok(_) => unreachable!(),
501 Err(e) => assert_eq!(e, ParsingError::MissingLocation {}),
502 };
503
504 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c";
505 match Entry::new(e1) {
506 Ok(_) => unreachable!(),
507 Err(e) => assert_eq!(
508 e,
509 ParsingError::MissingToken {
510 t: Token::LineNumber
511 }
512 ),
513 };
514
515 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c:fuzz";
516 match Entry::new(e1) {
517 Ok(_) => unreachable!(),
518 Err(e) => assert_eq!(
519 e,
520 ParsingError::InvalidLineNumber {
521 line: "fuzz".to_string()
522 }
523 ),
524 };
525
526 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c:510";
527 match Entry::new(e1) {
528 Ok(_) => unreachable!(),
529 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::Function }),
530 };
531
532 let e1 = "00:00:00.007773544 8874 0x558951015c00 INFO GST_INIT gst.c:510:";
533 match Entry::new(e1) {
534 Ok(_) => unreachable!(),
535 Err(e) => assert_eq!(e, ParsingError::MissingToken { t: Token::Object }),
536 };
537 }
538}