syslogio/
lib.rs

1use std::os::unix::net::UnixDatagram;
2use std::str::from_utf8;
3use std::error::Error;
4use std::fmt::{self, Display};
5use std::net::Shutdown;
6use problem::prelude::*;
7use problem::result::Result as PResult;
8use std::path::PathBuf;
9use std::io::{Write, Error as IoError, ErrorKind};
10use chrono::offset::Local;
11use chrono::{DateTime, FixedOffset};
12use itertools::Itertools;
13use log::*;
14
15#[derive(Debug)]
16pub struct SyslogPriorityNameError;
17
18impl Display for SyslogPriorityNameError {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        write!(f, "invalid syslog facility or severity name")
21    }
22}
23impl Error for SyslogPriorityNameError { }
24
25const FACILITY: [&str; 24] = ["kern", "user", "mail", "daemon", "auth", "syslog", "lpr", "news", "uucp", "cron", "authpriv", "ftp", "ntp", "audit", "alert", "clockd", "local0", "local1", "local2", "local3", "local4", "local5", "local6", "local7"];
26
27pub fn facility_by_id(facility: u8) -> Option<&'static str> {
28    FACILITY.get(facility as usize).map(|s| *s)
29}
30
31pub fn facility_by_name(facility: &str) -> Result<u8, SyslogPriorityNameError> {
32    FACILITY
33        .iter()
34        .position(|name| *name == facility)
35        .map(|pos| pos as u8)
36        .ok_or(SyslogPriorityNameError)
37}
38
39const SEVERITY: [&str; 8] = ["emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"];
40
41pub fn severity_by_id(severity: u8) -> Option<&'static str> {
42    SEVERITY.get(severity as usize).map(|s| *s)
43}
44
45pub fn severity_by_name(facility: &str) -> Result<u8, SyslogPriorityNameError> {
46    SEVERITY
47        .iter()
48        .position(|name| *name == facility)
49        .map(|pos| pos as u8)
50        .ok_or(SyslogPriorityNameError)
51}
52
53#[derive(Debug)]
54pub struct SyslogHeader<'s> {
55    pub facility: u8,
56    pub severity: u8,
57    pub timestamp: DateTime<FixedOffset>,
58    pub hostname: Option<&'s [u8]>,
59    pub tag: Option<&'s [u8]>,
60}
61
62// escape new lines to pereserve message boundries
63pub fn write_syslog_message(out: &mut impl Write, message: &[u8], escape: &[u8]) -> Result<(), IoError> {
64    for (no, line) in message.split(|c| *c == b'\n').enumerate() {
65        if no != 0 {
66            out.write_all(escape)?;
67        }
68        out.write_all(line)?;
69    }
70    Ok(())
71}
72
73impl<'s> SyslogHeader<'s> {
74    pub fn new(timestamp: DateTime<FixedOffset>, facility: &str, severity: &str, tag: &'s str) -> Result<SyslogHeader<'s>, SyslogPriorityNameError> {
75        Ok(SyslogHeader {
76            facility: facility_by_name(facility)?,
77            severity: severity_by_name(severity)?,
78            timestamp,
79            hostname: None,
80            tag: Some(tag.as_bytes()),
81        })
82    }
83
84    pub fn new_raw(timestamp: DateTime<FixedOffset>, facility: u8, severity: u8, tag: &'s [u8]) -> SyslogHeader<'s> {
85        SyslogHeader {
86            facility,
87            severity,
88            timestamp,
89            hostname: None,
90            tag: Some(tag),
91        }
92    }
93
94    pub fn hostname(&mut self, hostname: &'s[u8]) {
95        self.hostname = Some(hostname);
96    }
97
98    pub fn write(&self, out: &mut impl Write, timestamp_rfc3339: bool) -> Result<(), IoError> {
99        write!(out, "<{}>{} ",
100            ((self.facility as i32) << 3) + (self.severity as i32),
101            if timestamp_rfc3339 {
102                self.timestamp.format("%Y-%m-%dT%H:%M:%S%.6f%:z")
103            } else {
104                self.timestamp.format("%b %e %T")
105            }
106        )?;
107        if let Some(hostname) = self.hostname {
108            write_syslog_message(out, hostname, b"")?;
109            out.write_all(b" ")?;
110        }
111        write_syslog_message(out, self.tag.unwrap_or(b"syslog"), b"")?;
112        out.write_all(b": ")?;
113
114        Ok(())
115    }
116
117    pub fn parse(message: &[u8], timestamp: Option<DateTime<FixedOffset>>) -> Option<(SyslogHeader, &[u8])> {
118        let (pri, message) = message
119            .strip_prefix(b"<")?
120            .splitn(2, |c| *c == b'>')
121            .collect_tuple()?;
122
123        let pri: u8 = from_utf8(pri).ok()?.parse().ok()?;
124        let facility = pri >> 3;
125        let severity = pri & 0x7;
126
127        // Some if hight precision timestamp was available
128        let mut timestamp_rfc3339 = None;
129
130        let message = if let (Some(b' '), Some(b' '), Some(b':'), Some(b':'), Some(b' ')) =
131            (message.get(3), message.get(6), message.get(9), message.get(12), message.get(15)) {
132            // Apr 14 15:31:38
133            message.split_at(16).1
134        } else if let (Some(b'-'), Some(b'-'), Some(b'T'), Some((date, message))) =
135            (message.get(4), message.get(7), message.get(10), message.splitn(2, |c| *c == b' ').collect_tuple()) {
136            // 2022-04-14T15:34:45.327514+00:00
137            let date = String::from_utf8_lossy(date);
138            timestamp_rfc3339 = DateTime::parse_from_rfc3339(&date).ok();
139            message
140        } else if let Some((_sec_since_boot, message)) =
141            message.strip_prefix(b"[").and_then(|m| m.splitn(2, |c| *c == b']').collect_tuple()) {
142            // [23540.809085]
143            message.strip_prefix(b" ").unwrap_or(message)
144        } else {
145            message
146        };
147
148        let (tag, message) = if let Some(tag_end) = message.windows(2).position(|c| *c == [b':', b' ']) {
149            let (tag, message) = message.split_at(tag_end);
150            (Some(tag), &message[2..])
151        } else {
152            (None, message)
153        };
154
155        if timestamp_rfc3339.is_some() {
156            debug!("using RFC3339 timestamp from syslog header");
157        }
158
159        Some((SyslogHeader {
160            facility,
161            severity,
162            timestamp: timestamp_rfc3339.or(timestamp).unwrap_or_else(|| Local::now().into()),
163            hostname: None,
164            tag,
165        }, message))
166    }
167}
168
169#[derive(Debug)]
170pub struct SyslogUnix<'b> {
171    path: PathBuf,
172    socket: UnixDatagram,
173    buf: &'b mut [u8],
174}
175
176impl<'b> SyslogUnix<'b> {
177    pub fn new(buf: &'b mut [u8], path: impl Into<PathBuf>) -> PResult<SyslogUnix<'b>> {
178        let path = path.into();
179        let socket = UnixDatagram::unbound().problem_while("creating syslog socket")?;
180        socket.connect(&path).problem_while("connecting to syslog socket")?;
181
182        Ok(SyslogUnix {
183            path,
184            socket,
185            buf,
186        })
187    }
188
189    pub fn close(self) -> Result<(), IoError> {
190        self.socket.shutdown(Shutdown::Both)?;
191        Ok(())
192    }
193
194    pub fn datagram(&mut self, syslog: &SyslogHeader, timestamp_rfc3339: bool) -> Result<SyslogDatagram, IoError> {
195        let mut sd = SyslogDatagram {
196            buf_len: self.buf.len(),
197            datagram: self.buf,
198        };
199
200        syslog.write(&mut sd, timestamp_rfc3339)?;
201        Ok(sd)
202    }
203
204    pub fn with_datagram_send<O>(&mut self, syslog: &SyslogHeader, timestamp_rfc3339: bool, f: impl FnOnce(&mut SyslogDatagram) -> PResult<O> ) -> PResult<O> {
205        let mut dgram = self.datagram(syslog, timestamp_rfc3339).problem_while("writing syslog header")?;
206
207        let ret = f(&mut dgram)?;
208
209        let len = dgram.bytes_written();
210        self.send(len).problem_while("sending message to syslog socket")?;
211        Ok(ret)
212    }
213
214    pub fn send(&mut self, datagram_len: usize) -> Result<usize, IoError> {
215        let datagram = &self.buf[..datagram_len];
216        trace!("{}", String::from_utf8_lossy(datagram));
217        let len = loop {
218            match self.socket.send(datagram) {
219                Ok(len) => break Ok(len),
220                Err(err) if err.kind() == ErrorKind::NotConnected => {
221                    self.socket.connect(&self.path)?;
222                    debug!("deconnected to socket: {:?}", self.path);
223                }
224                Err(err) => break Err(err),
225            };
226        }?;
227        debug!("sent {} bytes datagram", len);
228        Ok(len)
229    }
230}
231
232#[derive(Debug)]
233pub struct SyslogDatagram<'b> {
234    buf_len: usize,
235    datagram: &'b mut [u8],
236}
237
238impl<'b> Write for SyslogDatagram<'b> {
239    fn write(&mut self, buf: &[u8]) -> Result<usize, IoError> {
240        self.datagram.write(buf)
241    }
242
243    fn flush(&mut self) -> Result<(), IoError> {
244        self.datagram.flush()
245    }
246}
247
248impl<'b> SyslogDatagram<'b> {
249    pub fn bytes_written(self) -> usize {
250        self.buf_len - self.datagram.len()
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_parse_minimal() {
260        let ts = DateTime::parse_from_rfc3339("2022-04-14T15:34:45.327514+00:00").unwrap();
261        let (hdr, msg) = SyslogHeader::parse(b"<5>hello world!", Some(ts)).unwrap();
262        assert_eq!(facility_by_id(hdr.facility).unwrap(), "kern");
263        assert_eq!(severity_by_id(hdr.severity).unwrap(), "notice");
264        assert_eq!(hdr.timestamp, ts);
265        assert_eq!(String::from_utf8(msg.to_vec()).unwrap(), "hello world!");
266    }
267
268    #[test]
269    fn test_parse_klog() {
270        let ts = DateTime::parse_from_rfc3339("2022-04-14T15:34:45.327514+00:00").unwrap();
271        let (hdr, msg) = SyslogHeader::parse(b"<3>[23540.809085] usb 1-1: 3:1: cannot get min/max values for control 2 (id 3)", Some(ts)).unwrap();
272        assert_eq!(facility_by_id(hdr.facility).unwrap(), "kern");
273        assert_eq!(severity_by_id(hdr.severity).unwrap(), "err");
274        assert_eq!(hdr.timestamp, ts);
275        assert_eq!(String::from_utf8(hdr.tag.unwrap().to_vec()).unwrap(), "usb 1-1");
276        assert_eq!(String::from_utf8(msg.to_vec()).unwrap(), "3:1: cannot get min/max values for control 2 (id 3)");
277    }
278
279    #[test]
280    fn test_parse_bsd() {
281        let ts = DateTime::parse_from_rfc3339("2022-04-14T15:34:45.327514+00:00").unwrap();
282        let (hdr, msg) = SyslogHeader::parse(b"<13>Apr 25 15:46:05 fred: hello world", Some(ts)).unwrap();
283        assert_eq!(facility_by_id(hdr.facility).unwrap(), "user");
284        assert_eq!(severity_by_id(hdr.severity).unwrap(), "notice");
285        assert_eq!(hdr.timestamp, ts);
286        assert_eq!(String::from_utf8(hdr.tag.unwrap().to_vec()).unwrap(), "fred");
287        assert_eq!(String::from_utf8(msg.to_vec()).unwrap(), "hello world");
288    }
289
290    #[test]
291    fn test_parse_rfs3339() {
292        let (hdr, msg) = SyslogHeader::parse(b"<13>2022-04-14T15:34:45.327514+00:00 fred: hello world", None).unwrap();
293        assert_eq!(facility_by_id(hdr.facility).unwrap(), "user");
294        assert_eq!(severity_by_id(hdr.severity).unwrap(), "notice");
295        assert_eq!(hdr.timestamp, DateTime::parse_from_rfc3339("2022-04-14T15:34:45.327514+00:00").unwrap());
296        assert_eq!(String::from_utf8(hdr.tag.unwrap().to_vec()).unwrap(), "fred");
297        assert_eq!(String::from_utf8(msg.to_vec()).unwrap(), "hello world");
298    }
299}