Skip to main content

syslog_too/
format.rs

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