1use crate::level::LogLevel;
2
3use std::fmt;
4
5pub(crate) const UUID_LEN: usize = 36;
7
8pub(crate) const UUID_PREFIX_LEN: usize = UUID_LEN + 1;
11
12#[non_exhaustive]
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum LineKind {
21 Full,
23 System,
25 UuidContinuation,
27 BareContinuation,
29 Truncated,
31 Empty,
33}
34
35impl fmt::Display for LineKind {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 match self {
38 LineKind::Full => f.pad("full"),
39 LineKind::System => f.pad("system"),
40 LineKind::UuidContinuation => f.pad("uuid-cont"),
41 LineKind::BareContinuation => f.pad("bare-cont"),
42 LineKind::Truncated => f.pad("truncated"),
43 LineKind::Empty => f.pad("empty"),
44 }
45 }
46}
47
48#[derive(Debug, PartialEq, Eq)]
54pub struct RawLine<'a> {
55 pub uuid: Option<&'a str>,
57 pub timestamp: Option<&'a str>,
59 pub idle_pct: Option<&'a str>,
61 pub level: Option<LogLevel>,
63 pub source: Option<&'a str>,
65 pub message: &'a str,
67 pub kind: LineKind,
69}
70
71pub(crate) fn is_uuid_at(bytes: &[u8], offset: usize) -> bool {
72 if bytes.len() < offset + UUID_PREFIX_LEN {
73 return false;
74 }
75 if bytes[offset + UUID_LEN] != b' ' {
76 return false;
77 }
78 for (i, &b) in bytes[offset..offset + UUID_LEN].iter().enumerate() {
79 match i {
80 8 | 13 | 18 | 23 => {
81 if b != b'-' {
82 return false;
83 }
84 }
85 _ => {
86 if !b.is_ascii_hexdigit() {
87 return false;
88 }
89 }
90 }
91 }
92 true
93}
94
95fn find_uuid_in(bytes: &[u8]) -> Option<usize> {
96 if bytes.len() < UUID_PREFIX_LEN {
97 return None;
98 }
99 let max_start = (bytes.len() - UUID_PREFIX_LEN).min(50);
100 (1..=max_start).find(|&start| is_uuid_at(bytes, start))
101}
102
103pub(crate) fn is_date_at(bytes: &[u8], offset: usize) -> bool {
104 if bytes.len() < offset + 5 {
105 return false;
106 }
107 bytes[offset..offset + 4].iter().all(u8::is_ascii_digit) && bytes[offset + 4] == b'-'
108}
109
110pub(crate) fn is_log_header_at(bytes: &[u8], offset: usize) -> bool {
116 if bytes.len() < offset + 31 {
118 return false;
119 }
120 if !(bytes[offset..offset + 4].iter().all(u8::is_ascii_digit)
122 && bytes[offset + 4] == b'-'
123 && bytes[offset + 5..offset + 7].iter().all(u8::is_ascii_digit)
124 && bytes[offset + 7] == b'-'
125 && bytes[offset + 8..offset + 10]
126 .iter()
127 .all(u8::is_ascii_digit)
128 && bytes[offset + 10] == b' '
129 && bytes[offset + 11..offset + 13]
130 .iter()
131 .all(u8::is_ascii_digit)
132 && bytes[offset + 13] == b':'
133 && bytes[offset + 14..offset + 16]
134 .iter()
135 .all(u8::is_ascii_digit)
136 && bytes[offset + 16] == b':'
137 && bytes[offset + 17..offset + 19]
138 .iter()
139 .all(u8::is_ascii_digit)
140 && bytes[offset + 19] == b'.'
141 && bytes[offset + 20..offset + 26]
142 .iter()
143 .all(u8::is_ascii_digit)
144 && bytes[offset + 26] == b' ')
145 {
146 return false;
147 }
148 let rest = &bytes[offset + 27..];
150 if !rest[0].is_ascii_digit() {
151 return false;
152 }
153 let Some(pct_pos) = rest[..rest.len().min(7)].iter().position(|&b| b == b'%') else {
154 return false;
155 };
156 rest.len() > pct_pos + 2 && rest[pct_pos + 1] == b' ' && rest[pct_pos + 2] == b'['
157}
158
159fn parse_idle_pct(rest: &str) -> (Option<&str>, &str) {
168 let bytes = rest.as_bytes();
169 if bytes.is_empty() || !bytes[0].is_ascii_digit() {
170 return (None, rest);
171 }
172 let search_len = rest.len().min(7);
173 let pct_pos = match bytes[..search_len].iter().position(|&b| b == b'%') {
174 Some(p) => p,
175 None => return (None, rest),
176 };
177 if !bytes[..pct_pos]
178 .iter()
179 .all(|&b| b.is_ascii_digit() || b == b'.')
180 {
181 return (None, rest);
182 }
183 let idle_pct = &rest[0..=pct_pos];
184 let after = if rest.len() > pct_pos + 2 {
185 &rest[pct_pos + 2..]
186 } else {
187 ""
188 };
189 (Some(idle_pct), after)
190}
191
192fn parse_timestamped_fields(
193 s: &str,
194) -> (
195 Option<&str>,
196 Option<&str>,
197 Option<LogLevel>,
198 Option<&str>,
199 &str,
200) {
201 if s.len() < 27 {
202 return (None, None, None, None, s);
203 }
204 let timestamp = &s[0..26];
205 let rest = &s[27..];
206
207 let (idle_pct, rest) = parse_idle_pct(rest);
208
209 let bracket_end = match rest.find(']') {
210 Some(p) => p,
211 None => return (Some(timestamp), idle_pct, None, None, rest),
212 };
213 let level = LogLevel::from_bracketed(&rest[0..=bracket_end]);
214
215 if rest.len() < bracket_end + 3 {
216 return (Some(timestamp), idle_pct, level, None, "");
217 }
218 let rest = &rest[bracket_end + 2..];
219
220 let source_end = rest.find(' ').unwrap_or(rest.len());
221 let source = &rest[0..source_end];
222 let message = if source_end < rest.len() {
223 &rest[source_end + 1..]
224 } else {
225 ""
226 };
227
228 (Some(timestamp), idle_pct, level, Some(source), message)
229}
230
231pub fn parse_line(line: &str) -> RawLine<'_> {
237 if line.trim().is_empty() {
238 return RawLine {
239 uuid: None,
240 timestamp: None,
241 idle_pct: None,
242 level: None,
243 source: None,
244 message: line,
245 kind: LineKind::Empty,
246 };
247 }
248
249 let bytes = line.as_bytes();
250
251 if is_uuid_at(bytes, 0) {
252 let uuid = &line[0..UUID_LEN];
253 let after_uuid = &line[UUID_PREFIX_LEN..];
254
255 if is_date_at(bytes, UUID_PREFIX_LEN) {
256 let (timestamp, idle_pct, level, source, message) =
257 parse_timestamped_fields(after_uuid);
258 return RawLine {
259 uuid: Some(uuid),
260 timestamp,
261 idle_pct,
262 level,
263 source,
264 message,
265 kind: LineKind::Full,
266 };
267 }
268
269 return RawLine {
270 uuid: Some(uuid),
271 timestamp: None,
272 idle_pct: None,
273 level: None,
274 source: None,
275 message: after_uuid,
276 kind: LineKind::UuidContinuation,
277 };
278 }
279
280 if is_date_at(bytes, 0) {
281 let (timestamp, idle_pct, level, source, message) = parse_timestamped_fields(line);
282 let (uuid, message) = if is_uuid_at(message.as_bytes(), 0) {
283 (Some(&message[0..UUID_LEN]), &message[UUID_PREFIX_LEN..])
284 } else {
285 (None, message)
286 };
287 return RawLine {
288 uuid,
289 timestamp,
290 idle_pct,
291 level,
292 source,
293 message,
294 kind: LineKind::System,
295 };
296 }
297
298 if let Some(uuid_start) = find_uuid_in(bytes) {
299 let uuid = &line[uuid_start..uuid_start + UUID_LEN];
300 let message = if line.len() > uuid_start + UUID_PREFIX_LEN {
301 &line[uuid_start + UUID_PREFIX_LEN..]
302 } else {
303 ""
304 };
305 return RawLine {
306 uuid: Some(uuid),
307 timestamp: None,
308 idle_pct: None,
309 level: None,
310 source: None,
311 message,
312 kind: LineKind::Truncated,
313 };
314 }
315
316 RawLine {
317 uuid: None,
318 timestamp: None,
319 idle_pct: None,
320 level: None,
321 source: None,
322 message: line,
323 kind: LineKind::BareContinuation,
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use super::*;
330
331 const UUID1: &str = "a1b2c3d4-e5f6-7890-abcd-ef1234567890";
332
333 #[test]
336 fn full_line_all_fields() {
337 let line = format!(
338 "{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Test message here"
339 );
340 let parsed = parse_line(&line);
341 assert_eq!(parsed.kind, LineKind::Full);
342 assert_eq!(parsed.uuid, Some(UUID1));
343 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
344 assert_eq!(parsed.idle_pct, Some("95.97%"));
345 assert_eq!(parsed.level, Some(LogLevel::Debug));
346 assert_eq!(parsed.source, Some("sofia.c:100"));
347 assert_eq!(parsed.message, "Test message here");
348 }
349
350 #[test]
351 fn full_line_each_level() {
352 for (name, expected) in [
353 ("DEBUG", LogLevel::Debug),
354 ("INFO", LogLevel::Info),
355 ("NOTICE", LogLevel::Notice),
356 ("WARNING", LogLevel::Warning),
357 ("ERR", LogLevel::Err),
358 ("CRIT", LogLevel::Crit),
359 ("ALERT", LogLevel::Alert),
360 ("CONSOLE", LogLevel::Console),
361 ] {
362 let line =
363 format!("{UUID1} 2025-01-15 10:30:45.123456 95.97% [{name}] sofia.c:100 Test");
364 let parsed = parse_line(&line);
365 assert_eq!(parsed.kind, LineKind::Full);
366 assert_eq!(parsed.level, Some(expected), "failed for [{name}]");
367 }
368 }
369
370 #[test]
371 fn full_line_high_idle() {
372 let line =
373 format!("{UUID1} 2025-01-15 10:30:45.123456 99.99% [DEBUG] sofia.c:100 High idle");
374 let parsed = parse_line(&line);
375 assert_eq!(parsed.idle_pct, Some("99.99%"));
376 }
377
378 #[test]
379 fn full_line_low_idle() {
380 let line = format!("{UUID1} 2025-01-15 10:30:45.123456 0.00% [DEBUG] sofia.c:100 Low idle");
381 let parsed = parse_line(&line);
382 assert_eq!(parsed.idle_pct, Some("0.00%"));
383 }
384
385 #[test]
386 fn full_line_long_message() {
387 let line = format!(
388 "{UUID1} 2025-01-15 10:30:45.123456 95.97% [DEBUG] sofia.c:100 Channel [sofia/internal] key=val:123 (test) {{braces}}"
389 );
390 let parsed = parse_line(&line);
391 assert_eq!(
392 parsed.message,
393 "Channel [sofia/internal] key=val:123 (test) {braces}"
394 );
395 }
396
397 #[test]
400 fn system_line_no_uuid() {
401 let line =
402 "2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
403 let parsed = parse_line(line);
404 assert_eq!(parsed.kind, LineKind::System);
405 assert_eq!(parsed.uuid, None);
406 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
407 assert_eq!(parsed.idle_pct, Some("95.97%"));
408 assert_eq!(parsed.level, Some(LogLevel::Info));
409 assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
410 assert_eq!(parsed.message, "Event Socket command");
411 }
412
413 #[test]
414 fn system_line_with_embedded_uuid() {
415 let line = format!(
416 "2025-01-15 10:30:45.123456 95.97% [DEBUG] switch_cpp.cpp:1466 {UUID1} DAA-LOG WaveManager PSAP 911 originate"
417 );
418 let parsed = parse_line(&line);
419 assert_eq!(parsed.kind, LineKind::System);
420 assert_eq!(parsed.uuid, Some(UUID1));
421 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
422 assert_eq!(parsed.level, Some(LogLevel::Debug));
423 assert_eq!(parsed.source, Some("switch_cpp.cpp:1466"));
424 assert_eq!(parsed.message, "DAA-LOG WaveManager PSAP 911 originate");
425 }
426
427 #[test]
428 fn system_line_with_embedded_uuid_empty_message() {
429 let line = format!("2025-01-15 10:30:45.123456 95.97% [INFO] switch_cpp.cpp:1466 {UUID1} ");
430 let parsed = parse_line(&line);
431 assert_eq!(parsed.kind, LineKind::System);
432 assert_eq!(parsed.uuid, Some(UUID1));
433 assert_eq!(parsed.message, "");
434 }
435
436 #[test]
437 fn system_line_without_embedded_uuid() {
438 let line =
439 "2025-01-15 10:30:45.123456 95.97% [INFO] mod_event_socket.c:1772 Event Socket command";
440 let parsed = parse_line(line);
441 assert_eq!(parsed.kind, LineKind::System);
442 assert_eq!(parsed.uuid, None);
443 assert_eq!(parsed.message, "Event Socket command");
444 }
445
446 #[test]
447 fn system_line_event_socket() {
448 let line = "2025-01-15 10:30:45.123456 95.97% [NOTICE] mod_logfile.c:217 New log started.";
449 let parsed = parse_line(line);
450 assert_eq!(parsed.kind, LineKind::System);
451 assert_eq!(parsed.level, Some(LogLevel::Notice));
452 assert_eq!(parsed.message, "New log started.");
453 }
454
455 #[test]
458 fn uuid_continuation_dialplan() {
459 let line =
460 format!("{UUID1} Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]");
461 let parsed = parse_line(&line);
462 assert_eq!(parsed.kind, LineKind::UuidContinuation);
463 assert_eq!(parsed.uuid, Some(UUID1));
464 assert_eq!(parsed.timestamp, None);
465 assert_eq!(parsed.level, None);
466 assert_eq!(
467 parsed.message,
468 "Dialplan: sofia/internal/+15550001234@192.0.2.1 parsing [public]"
469 );
470 }
471
472 #[test]
473 fn uuid_continuation_execute() {
474 let line =
475 format!("{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)");
476 let parsed = parse_line(&line);
477 assert_eq!(parsed.kind, LineKind::UuidContinuation);
478 assert_eq!(parsed.uuid, Some(UUID1));
479 assert_eq!(
480 parsed.message,
481 "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(foo=bar)"
482 );
483 }
484
485 #[test]
486 fn uuid_continuation_channel_var() {
487 let line = format!("{UUID1} Channel-State: [CS_EXECUTE]");
488 let parsed = parse_line(&line);
489 assert_eq!(parsed.kind, LineKind::UuidContinuation);
490 assert_eq!(parsed.uuid, Some(UUID1));
491 assert_eq!(parsed.message, "Channel-State: [CS_EXECUTE]");
492 }
493
494 #[test]
495 fn uuid_continuation_variable() {
496 let line = format!("{UUID1} variable_sip_call_id: [test123@192.0.2.1]");
497 let parsed = parse_line(&line);
498 assert_eq!(parsed.kind, LineKind::UuidContinuation);
499 assert_eq!(parsed.uuid, Some(UUID1));
500 assert_eq!(parsed.message, "variable_sip_call_id: [test123@192.0.2.1]");
501 }
502
503 #[test]
504 fn uuid_continuation_blank() {
505 let line = format!("{UUID1} ");
506 let parsed = parse_line(&line);
507 assert_eq!(parsed.kind, LineKind::UuidContinuation);
508 assert_eq!(parsed.uuid, Some(UUID1));
509 assert_eq!(parsed.message, "");
510 }
511
512 #[test]
515 fn bare_variable() {
516 let line = "variable_foo: [bar]";
517 let parsed = parse_line(line);
518 assert_eq!(parsed.kind, LineKind::BareContinuation);
519 assert_eq!(parsed.uuid, None);
520 assert_eq!(parsed.message, "variable_foo: [bar]");
521 }
522
523 #[test]
524 fn bare_sdp_origin() {
525 let line = "o=- 1234 5678 IN IP4 192.0.2.1";
526 let parsed = parse_line(line);
527 assert_eq!(parsed.kind, LineKind::BareContinuation);
528 assert_eq!(parsed.message, line);
529 }
530
531 #[test]
532 fn bare_sdp_media() {
533 let line = "m=audio 47758 RTP/AVP 0 101";
534 let parsed = parse_line(line);
535 assert_eq!(parsed.kind, LineKind::BareContinuation);
536 assert_eq!(parsed.message, line);
537 }
538
539 #[test]
540 fn bare_sdp_attribute() {
541 let line = "a=rtpmap:0 PCMU/8000";
542 let parsed = parse_line(line);
543 assert_eq!(parsed.kind, LineKind::BareContinuation);
544 assert_eq!(parsed.message, line);
545 }
546
547 #[test]
548 fn bare_closing_bracket() {
549 let line = "]";
550 let parsed = parse_line(line);
551 assert_eq!(parsed.kind, LineKind::BareContinuation);
552 assert_eq!(parsed.message, "]");
553 }
554
555 #[test]
556 fn bare_empty_line() {
557 let parsed = parse_line("");
558 assert_eq!(parsed.kind, LineKind::Empty);
559 assert_eq!(parsed.message, "");
560 }
561
562 #[test]
565 fn truncated_varia_prefix() {
566 let line = format!(
567 "varia{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
568 );
569 let parsed = parse_line(&line);
570 assert_eq!(parsed.kind, LineKind::Truncated);
571 assert_eq!(parsed.uuid, Some(UUID1));
572 assert_eq!(
573 parsed.message,
574 "EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
575 );
576 }
577
578 #[test]
579 fn truncated_variab_prefix() {
580 let line = format!(
581 "variab{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
582 );
583 let parsed = parse_line(&line);
584 assert_eq!(parsed.kind, LineKind::Truncated);
585 assert_eq!(parsed.uuid, Some(UUID1));
586 }
587
588 #[test]
589 fn truncated_var_prefix() {
590 let line =
591 format!("var{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)");
592 let parsed = parse_line(&line);
593 assert_eq!(parsed.kind, LineKind::Truncated);
594 assert_eq!(parsed.uuid, Some(UUID1));
595 }
596
597 #[test]
598 fn truncated_variable_prefix() {
599 let line = format!(
600 "variable{UUID1} EXECUTE [depth=0] sofia/internal/+15550001234@192.0.2.1 set(x=y)"
601 );
602 let parsed = parse_line(&line);
603 assert_eq!(parsed.kind, LineKind::Truncated);
604 assert_eq!(parsed.uuid, Some(UUID1));
605 }
606
607 #[test]
610 fn full_line_no_idle_pct() {
611 let line = format!(
612 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 ended"
613 );
614 let parsed = parse_line(&line);
615 assert_eq!(parsed.kind, LineKind::Full);
616 assert_eq!(parsed.uuid, Some(UUID1));
617 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
618 assert_eq!(parsed.idle_pct, None);
619 assert_eq!(parsed.level, Some(LogLevel::Notice));
620 assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
621 assert_eq!(parsed.message, "Session 3178948 ended");
622 }
623
624 #[test]
625 fn full_line_no_idle_pct_url_encoded_percent() {
626 let line = format!(
627 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] switch_core_session.c:1744 Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
628 );
629 let parsed = parse_line(&line);
630 assert_eq!(parsed.kind, LineKind::Full);
631 assert_eq!(parsed.uuid, Some(UUID1));
632 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
633 assert_eq!(parsed.idle_pct, None);
634 assert_eq!(parsed.level, Some(LogLevel::Notice));
635 assert_eq!(parsed.source, Some("switch_core_session.c:1744"));
636 assert_eq!(
637 parsed.message,
638 "Session 3178948 (sofia/psap/gw%2Bsg1vofswb-inbound@198.51.100.5:5060) Ended"
639 );
640 }
641
642 #[test]
643 fn system_line_no_idle_pct() {
644 let line = "2025-01-15 10:30:45.123456 [INFO] mod_event_socket.c:1772 Event Socket command";
645 let parsed = parse_line(line);
646 assert_eq!(parsed.kind, LineKind::System);
647 assert_eq!(parsed.uuid, None);
648 assert_eq!(parsed.timestamp, Some("2025-01-15 10:30:45.123456"));
649 assert_eq!(parsed.idle_pct, None);
650 assert_eq!(parsed.level, Some(LogLevel::Info));
651 assert_eq!(parsed.source, Some("mod_event_socket.c:1772"));
652 assert_eq!(parsed.message, "Event Socket command");
653 }
654
655 #[test]
656 fn full_line_no_idle_pct_hangup_url_encoded() {
657 let line = format!(
658 "{UUID1} 2025-01-15 10:30:45.123456 [NOTICE] sofia.c:1089 Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
659 );
660 let parsed = parse_line(&line);
661 assert_eq!(parsed.kind, LineKind::Full);
662 assert_eq!(parsed.idle_pct, None);
663 assert_eq!(parsed.level, Some(LogLevel::Notice));
664 assert_eq!(parsed.source, Some("sofia.c:1089"));
665 assert_eq!(
666 parsed.message,
667 "Hangup sofia/psap/gw%2Bgateway@198.51.100.5:5060 [CS_EXCHANGE_MEDIA] [CALL_AWARDED_DELIVERED]"
668 );
669 }
670
671 #[test]
674 fn not_uuid_36_chars() {
675 let line = "this-is-not-a-valid-uuid-value-12345 rest of line";
676 let parsed = parse_line(line);
677 assert_eq!(parsed.kind, LineKind::BareContinuation);
678 assert_eq!(parsed.message, line);
679 }
680
681 #[test]
682 fn uuid_in_message_not_prefix() {
683 let line =
684 format!("This is some log message body with extra context then {UUID1} appears here");
685 let parsed = parse_line(&line);
686 assert_eq!(parsed.kind, LineKind::BareContinuation);
687 assert_eq!(parsed.message, line.as_str());
688 }
689
690 #[test]
691 fn whitespace_only_is_empty() {
692 let parsed = parse_line(" \t ");
693 assert_eq!(parsed.kind, LineKind::Empty);
694 }
695}