@@ -12,6 +12,9 @@ enum SyslogFormat {
1212 /// rsyslog RSYSLOG_FileFormat: `ISO-timestamp hostname tag[pid]: message`
1313 /// (ISO 8601 timestamp with no `<PRI>` prefix).
1414 RsyslogIso ,
15+ /// rsyslog with Unix epoch timestamp: `1436735381.000000 hostname tag[pid]: message`
16+ /// (no `<PRI>` prefix).
17+ Unix ,
1518}
1619
1720impl SyslogFormat {
@@ -23,7 +26,8 @@ impl SyslogFormat {
2326 match i {
2427 0 => Self :: Rfc3164 ,
2528 1 => Self :: Rfc5424 ,
26- _ => Self :: RsyslogIso ,
29+ 2 => Self :: RsyslogIso ,
30+ _ => Self :: Unix ,
2731 }
2832 }
2933}
@@ -33,7 +37,7 @@ const MIN_SAMPLES: u32 = 50;
3337#[ derive( Debug , Default ) ]
3438pub struct SyslogParser {
3539 format : OnceLock < SyslogFormat > ,
36- fmt_counts : [ AtomicU32 ; 3 ] ,
40+ fmt_counts : [ AtomicU32 ; 4 ] ,
3741 fmt_total : AtomicU32 ,
3842}
3943
@@ -455,6 +459,8 @@ fn detect_syslog_timestamp<'a>(s: &'a str, line: &'a [u8]) -> Option<(SyslogForm
455459 }
456460 } else if let Some ( ( ts, _) ) = super :: timestamp:: parse_iso_timestamp ( body) {
457461 return Some ( ( SyslogFormat :: RsyslogIso , ts) ) ;
462+ } else if let Some ( ( ts, _) ) = super :: timestamp:: parse_unix_timestamp ( body) {
463+ return Some ( ( SyslogFormat :: Unix , ts) ) ;
458464 }
459465 }
460466 None
@@ -489,12 +495,45 @@ fn extract_syslog_timestamp_rsyslog_iso(s: &str) -> Option<&str> {
489495 super :: timestamp:: parse_iso_timestamp ( s) . map ( |( ts, _) | ts)
490496}
491497
498+ fn extract_syslog_timestamp_unix ( s : & str ) -> Option < & str > {
499+ super :: timestamp:: parse_unix_timestamp ( s) . map ( |( ts, _) | ts)
500+ }
501+
502+ /// Parse rsyslog with Unix epoch timestamp: `1436735381.000000 hostname tag[pid]: message`.
503+ /// No `<PRI>` prefix.
504+ fn parse_rsyslog_unix_inner < ' a > ( s : & ' a str ) -> Option < DisplayParts < ' a > > {
505+ let ( timestamp, ts_end) = super :: timestamp:: parse_unix_timestamp ( s) ?;
506+ let rest = s[ ts_end..] . strip_prefix ( ' ' ) ?;
507+
508+ let mut parts = DisplayParts {
509+ timestamp : Some ( timestamp) ,
510+ ..Default :: default ( )
511+ } ;
512+
513+ if rest. is_empty ( ) {
514+ return Some ( parts) ;
515+ }
516+
517+ let ( hostname, rest) = next_token ( rest) ?;
518+ if !is_valid_syslog_hostname ( hostname) {
519+ return None ;
520+ }
521+ push_field_as ( & mut parts. extra_fields , FieldSemantic :: Hostname , hostname) ;
522+
523+ if rest. is_empty ( ) {
524+ return Some ( parts) ;
525+ }
526+
527+ extract_tag_and_message ( rest, & mut parts) ;
528+ Some ( parts)
529+ }
530+
492531impl SyslogParser {
493532 fn record_format ( & self , fmt : SyslogFormat ) {
494533 self . fmt_counts [ fmt. index ( ) ] . fetch_add ( 1 , Ordering :: Relaxed ) ;
495534 let total = self . fmt_total . fetch_add ( 1 , Ordering :: Relaxed ) + 1 ;
496535 if total >= MIN_SAMPLES && self . format . get ( ) . is_none ( ) {
497- let winner = ( 0 ..3 )
536+ let winner = ( 0 ..4 )
498537 . max_by_key ( |& i| self . fmt_counts [ i] . load ( Ordering :: Relaxed ) )
499538 . unwrap_or ( 0 ) ;
500539 let _ = self . format . set ( SyslogFormat :: from_index ( winner) ) ;
@@ -517,6 +556,7 @@ impl LogFormatParser for SyslogParser {
517556 SyslogFormat :: Rfc3164 => extract_syslog_timestamp_rfc3164 ( s, line) ,
518557 SyslogFormat :: Rfc5424 => extract_syslog_timestamp_rfc5424 ( s, line) ,
519558 SyslogFormat :: RsyslogIso => extract_syslog_timestamp_rsyslog_iso ( s) ,
559+ SyslogFormat :: Unix => extract_syslog_timestamp_unix ( s) ,
520560 } ;
521561 }
522562 let ( fmt, ts) = detect_syslog_timestamp ( s, line) ?;
@@ -544,6 +584,7 @@ impl LogFormatParser for SyslogParser {
544584 }
545585 }
546586 SyslogFormat :: RsyslogIso => parse_rsyslog_iso_inner ( s) ,
587+ SyslogFormat :: Unix => parse_rsyslog_unix_inner ( s) ,
547588 } ;
548589 if result. is_some ( ) {
549590 return result;
@@ -572,6 +613,11 @@ impl LogFormatParser for SyslogParser {
572613 return Some ( parts) ;
573614 }
574615
616+ if let Some ( parts) = parse_rsyslog_unix_inner ( s) {
617+ self . record_format ( SyslogFormat :: Unix ) ;
618+ return Some ( parts) ;
619+ }
620+
575621 None
576622 }
577623
@@ -608,7 +654,7 @@ impl LogFormatParser for SyslogParser {
608654 /// • ISO timestamp (`YYYY-MM-DDTHH:MM:SS…`) — rsyslog RSYSLOG_FileFormat
609655 ///
610656 /// Plain BSD lines without a priority prefix (`Oct 11 22:14:15 host tag: msg`)
611- /// are shared with journalctl `--output short` and are intentionally **not**
657+ /// and Unix epoch lines are shared with journalctl and are intentionally **not**
612658 /// claimed here so that piped `journalctl` output is still detected as
613659 /// journalctl. Those lines can still be *parsed* by `parse_line` once the
614660 /// format is locked by other lines in the sample.
@@ -1029,4 +1075,40 @@ mod tests {
10291075 assert_eq ! ( before. level, after. level) ;
10301076 assert_eq ! ( before. target, after. target) ;
10311077 }
1078+
1079+ // ── Unix epoch (rsyslog custom template) ─────────────────────────
1080+
1081+ #[ test]
1082+ fn test_unix_epoch_basic ( ) {
1083+ let line = b"1436735381.000000 myhost sshd[1234]: Connection closed" ;
1084+ let parser = SyslogParser :: default ( ) ;
1085+ let parts = parser. parse_line ( line) . unwrap ( ) ;
1086+ assert_eq ! ( parts. timestamp, Some ( "1436735381.000000" ) ) ;
1087+ assert_eq ! ( parts. target, Some ( "sshd" ) ) ;
1088+ assert_eq ! ( parts. message, Some ( "Connection closed" ) ) ;
1089+ assert ! (
1090+ parts
1091+ . extra_fields
1092+ . iter( )
1093+ . any( |( _, k, v) | * k == "hostname" && * v == "myhost" )
1094+ ) ;
1095+ }
1096+
1097+ #[ test]
1098+ fn test_unix_epoch_parse_timestamp ( ) {
1099+ let line = b"1700000000.123456 myhost systemd[1]: Started service" ;
1100+ let parser = SyslogParser :: default ( ) ;
1101+ let ts = parser. parse_timestamp ( line) . unwrap ( ) ;
1102+ assert_eq ! ( ts, "1700000000.123456" ) ;
1103+ }
1104+
1105+ #[ test]
1106+ fn test_unix_epoch_timestamp_has_year ( ) {
1107+ let parser = SyslogParser :: default ( ) ;
1108+ let line = b"1436735381.000000 myhost sshd[1234]: Connection closed" ;
1109+ for _ in 0 ..MIN_SAMPLES {
1110+ parser. parse_line ( line) . unwrap ( ) ;
1111+ }
1112+ assert ! ( parser. timestamp_has_year( ) ) ;
1113+ }
10321114}
0 commit comments