syslog/
format.rs

1use std::collections::BTreeMap;
2use std::fmt::Display;
3use std::io::Write;
4use time;
5
6use errors::*;
7use facility::Facility;
8use get_hostname;
9use get_process_info;
10use Priority;
11
12#[allow(non_camel_case_types)]
13#[derive(Copy, Clone)]
14pub enum Severity {
15    LOG_EMERG,
16    LOG_ALERT,
17    LOG_CRIT,
18    LOG_ERR,
19    LOG_WARNING,
20    LOG_NOTICE,
21    LOG_INFO,
22    LOG_DEBUG,
23}
24
25pub trait LogFormat<T> {
26    fn format<W: Write>(&self, w: &mut W, severity: Severity, message: T) -> Result<()>;
27
28    fn emerg<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
29        self.format(w, Severity::LOG_EMERG, message)
30    }
31
32    fn alert<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
33        self.format(w, Severity::LOG_ALERT, message)
34    }
35
36    fn crit<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
37        self.format(w, Severity::LOG_CRIT, message)
38    }
39
40    fn err<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
41        self.format(w, Severity::LOG_ERR, message)
42    }
43
44    fn warning<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
45        self.format(w, Severity::LOG_WARNING, message)
46    }
47
48    fn notice<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
49        self.format(w, Severity::LOG_NOTICE, message)
50    }
51
52    fn info<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
53        self.format(w, Severity::LOG_INFO, message)
54    }
55
56    fn debug<W: Write>(&mut self, w: &mut W, message: T) -> Result<()> {
57        self.format(w, Severity::LOG_DEBUG, message)
58    }
59}
60
61#[derive(Clone, Debug)]
62pub struct Formatter3164 {
63    pub facility: Facility,
64    pub hostname: Option<String>,
65    pub process: String,
66    pub pid: u32,
67}
68
69impl<T: Display> LogFormat<T> for Formatter3164 {
70    fn format<W: Write>(&self, w: &mut W, severity: Severity, message: T) -> Result<()> {
71        let format =
72            time::format_description::parse("[month repr:short] [day] [hour]:[minute]:[second]")
73                .unwrap();
74
75        if let Some(ref hostname) = self.hostname {
76            write!(
77                w,
78                "<{}>{} {} {}[{}]: {}",
79                encode_priority(severity, self.facility),
80                now_local()
81                    .map(|timestamp| timestamp.format(&format).unwrap())
82                    .unwrap(),
83                hostname,
84                self.process,
85                self.pid,
86                message
87            )
88            .map_err(Error::Write)
89        } else {
90            write!(
91                w,
92                "<{}>{} {}[{}]: {}",
93                encode_priority(severity, self.facility),
94                now_local()
95                    .map(|timestamp| timestamp.format(&format).unwrap())
96                    .unwrap(),
97                self.process,
98                self.pid,
99                message
100            )
101            .map_err(Error::Write)
102        }
103    }
104}
105
106impl Default for Formatter3164 {
107    /// Returns a `Formatter3164` with default settings.
108    ///
109    /// The default settings are as follows:
110    ///
111    /// * `facility`: `LOG_USER`, as [specified by POSIX].
112    /// * `hostname`: Automatically detected using [the `hostname` crate], if possible.
113    /// * `process`: Automatically detected using [`std::env::current_exe`], or if that fails, an empty string.
114    /// * `pid`: Automatically detected using [`libc::getpid`].
115    ///
116    /// [`libc::getpid`]: https://docs.rs/libc/0.2/libc/fn.getpid.html
117    /// [specified by POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html
118    /// [`std::env::current_exe`]: https://doc.rust-lang.org/std/env/fn.current_exe.html
119    /// [the `hostname` crate]: https://crates.io/crates/hostname
120    fn default() -> Self {
121        let (process, pid) = get_process_info().unwrap_or((String::new(), std::process::id()));
122        let hostname = get_hostname().ok();
123
124        Self {
125            facility: Default::default(),
126            hostname,
127            process,
128            pid,
129        }
130    }
131}
132
133/// RFC 5424 structured data
134pub type StructuredData = BTreeMap<String, BTreeMap<String, String>>;
135
136#[derive(Clone, Debug)]
137pub struct Formatter5424 {
138    pub facility: Facility,
139    pub hostname: Option<String>,
140    pub process: String,
141    pub pid: u32,
142}
143
144impl Formatter5424 {
145    pub fn format_5424_structured_data(&self, data: StructuredData) -> String {
146        if data.is_empty() {
147            "-".to_string()
148        } else {
149            let mut res = String::new();
150            for (id, params) in &data {
151                res = res + "[" + id;
152                for (name, value) in params {
153                    res =
154                        res + " " + name + "=\"" + &escape_structure_data_param_value(value) + "\"";
155                }
156                res += "]";
157            }
158
159            res
160        }
161    }
162}
163
164impl<T: Display> LogFormat<(u32, StructuredData, T)> for Formatter5424 {
165    fn format<W: Write>(
166        &self,
167        w: &mut W,
168        severity: Severity,
169        log_message: (u32, StructuredData, T),
170    ) -> Result<()> {
171        let (message_id, data, message) = log_message;
172
173        // Guard against sub-second precision over 6 digits per rfc5424 section 6
174        let timestamp = time::OffsetDateTime::now_utc();
175        // SAFETY: timestamp range is enforced, so this will never fail
176        let timestamp = timestamp
177            // Removing significant figures beyond 6 digits
178            .replace_nanosecond(timestamp.nanosecond() / 1000 * 1000)
179            .unwrap();
180
181        write!(
182            w,
183            "<{}>1 {} {} {} {} {} {} {}", // v1
184            encode_priority(severity, self.facility),
185            timestamp
186                .format(&time::format_description::well_known::Rfc3339)
187                .unwrap(),
188            self.hostname
189                .as_ref()
190                .map(|x| &x[..])
191                .unwrap_or("localhost"),
192            self.process,
193            self.pid,
194            message_id,
195            self.format_5424_structured_data(data),
196            message
197        )
198        .map_err(Error::Write)
199    }
200}
201
202impl Default for Formatter5424 {
203    /// Returns a `Formatter5424` with default settings.
204    ///
205    /// The default settings are as follows:
206    ///
207    /// * `facility`: `LOG_USER`, as [specified by POSIX].
208    /// * `hostname`: Automatically detected using [the `hostname` crate], if possible.
209    /// * `process`: Automatically detected using [`std::env::current_exe`], or if that fails, an empty string.
210    /// * `pid`: Automatically detected using [`libc::getpid`].
211    ///
212    /// [`libc::getpid`]: https://docs.rs/libc/0.2/libc/fn.getpid.html
213    /// [specified by POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html
214    /// [`std::env::current_exe`]: https://doc.rust-lang.org/std/env/fn.current_exe.html
215    /// [the `hostname` crate]: https://crates.io/crates/hostname
216    fn default() -> Self {
217        // Get the defaults from `Formatter3164` and move them over.
218        let Formatter3164 {
219            facility,
220            hostname,
221            process,
222            pid,
223        } = Default::default();
224        Self {
225            facility,
226            hostname,
227            process,
228            pid,
229        }
230    }
231}
232
233fn escape_structure_data_param_value(value: &str) -> String {
234    value
235        .replace('\\', "\\\\")
236        .replace('"', "\\\"")
237        .replace(']', "\\]")
238}
239
240fn encode_priority(severity: Severity, facility: Facility) -> Priority {
241    facility as u8 | severity as u8
242}
243
244#[cfg(unix)]
245// On unix platforms, time::OffsetDateTime::now_local always returns an error so use UTC instead
246// https://github.com/time-rs/time/issues/380
247fn now_local() -> std::result::Result<time::OffsetDateTime, time::error::IndeterminateOffset> {
248    Ok(time::OffsetDateTime::now_utc())
249}
250
251#[cfg(not(unix))]
252fn now_local() -> std::result::Result<time::OffsetDateTime, time::error::IndeterminateOffset> {
253    time::OffsetDateTime::now_local()
254}
255
256#[cfg(test)]
257mod test {
258    use super::*;
259
260    #[test]
261    fn backslash_is_escaped() {
262        let string = "\\";
263        let value = escape_structure_data_param_value(string);
264        assert_eq!(value, "\\\\");
265    }
266    #[test]
267    fn quote_is_escaped() {
268        let string = "foo\"bar";
269        let value = escape_structure_data_param_value(string);
270        assert_eq!(value, "foo\\\"bar");
271    }
272    #[test]
273    fn end_bracket_is_escaped() {
274        let string = "]";
275        let value = escape_structure_data_param_value(string);
276        assert_eq!(value, "\\]");
277    }
278
279    #[test]
280    fn test_formatter3164_defaults() {
281        let d = Formatter3164::default();
282
283        // `Facility` doesn't implement `PartialEq`, so we use a `match` instead.
284        assert!(match d.facility {
285            Facility::LOG_USER => true,
286            _ => false,
287        });
288
289        assert!(match &d.hostname {
290            Some(hostname) => !hostname.is_empty(),
291            None => false,
292        });
293
294        assert!(!d.process.is_empty());
295
296        // Can't really make any assertions about the pid.
297    }
298
299    #[test]
300    fn test_formatter5424_defaults() {
301        let d = Formatter5424::default();
302
303        // `Facility` doesn't implement `PartialEq`, so we use a `match` instead.
304        assert!(match d.facility {
305            Facility::LOG_USER => true,
306            _ => false,
307        });
308
309        assert!(match &d.hostname {
310            Some(hostname) => !hostname.is_empty(),
311            None => false,
312        });
313
314        assert!(!d.process.is_empty());
315
316        // Can't really make any assertions about the pid.
317    }
318}