syslog_tls/
format.rs

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