syslog_tracing/
syslog.rs

1use std::{borrow::Cow, cell::RefCell, ffi::CStr, fmt, io, str, sync::atomic::AtomicBool};
2use tracing_core::{Level, Metadata};
3use tracing_subscriber::fmt::MakeWriter;
4
5/// `syslog` options.
6///
7/// # Examples
8/// ```
9/// use syslog_tracing::Options;
10/// // Log PID with messages and log to stderr as well as `syslog`.
11/// let opts = Options::LOG_PID | Options::LOG_PERROR;
12/// ```
13#[derive(Copy, Clone, Debug, Default)]
14pub struct Options(libc::c_int);
15
16impl Options {
17    /// Log the pid with each message.
18    pub const LOG_PID: Self = Self(libc::LOG_PID);
19    /// Log on the console if errors in sending.
20    pub const LOG_CONS: Self = Self(libc::LOG_CONS);
21    /// Delay open until first syslog() (default).
22    pub const LOG_ODELAY: Self = Self(libc::LOG_ODELAY);
23    /// Don't delay open.
24    pub const LOG_NDELAY: Self = Self(libc::LOG_NDELAY);
25    /// Don't wait for console forks: DEPRECATED.
26    pub const LOG_NOWAIT: Self = Self(libc::LOG_NOWAIT);
27    /// Log to stderr as well.
28    pub const LOG_PERROR: Self = Self(libc::LOG_PERROR);
29}
30
31impl std::ops::BitOr for Options {
32    type Output = Self;
33    fn bitor(self, rhs: Self) -> Self::Output {
34        Self(self.0 | rhs.0)
35    }
36}
37
38/// `syslog` facility.
39#[derive(Copy, Clone, Debug)]
40#[repr(i32)]
41pub enum Facility {
42    /// Generic user-level messages.
43    #[cfg_attr(docsrs, doc(alias = "LOG_USER"))]
44    User = libc::LOG_USER,
45    /// Mail subsystem.
46    #[cfg_attr(docsrs, doc(alias = "LOG_MAIL"))]
47    Mail = libc::LOG_MAIL,
48    /// System daemons without separate facility value.
49    #[cfg_attr(docsrs, doc(alias = "LOG_DAEMON"))]
50    Daemon = libc::LOG_DAEMON,
51    /// Security/authorization messages.
52    #[cfg_attr(docsrs, doc(alias = "LOG_AUTH"))]
53    Auth = libc::LOG_AUTH,
54    /// Line printer subsystem.
55    #[cfg_attr(docsrs, doc(alias = "LOG_LPR"))]
56    Lpr = libc::LOG_LPR,
57    /// USENET news subsystem.
58    #[cfg_attr(docsrs, doc(alias = "LOG_NEWS"))]
59    News = libc::LOG_NEWS,
60    /// UUCP subsystem.
61    #[cfg_attr(docsrs, doc(alias = "LOG_UUCP"))]
62    Uucp = libc::LOG_UUCP,
63    /// Clock daemon (`cron` and `at`).
64    #[cfg_attr(docsrs, doc(alias = "LOG_CRON"))]
65    Cron = libc::LOG_CRON,
66    /// Security/authorization messages (private).
67    #[cfg_attr(docsrs, doc(alias = "LOG_AUTHPRIV"))]
68    AuthPriv = libc::LOG_AUTHPRIV,
69    /// FTP daemon.
70    #[cfg_attr(docsrs, doc(alias = "LOG_FTP"))]
71    Ftp = libc::LOG_FTP,
72    /// Reserved for local use.
73    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL0"))]
74    Local0 = libc::LOG_LOCAL0,
75    /// Reserved for local use.
76    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL1"))]
77    Local1 = libc::LOG_LOCAL1,
78    /// Reserved for local use.
79    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL2"))]
80    Local2 = libc::LOG_LOCAL2,
81    /// Reserved for local use.
82    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL3"))]
83    Local3 = libc::LOG_LOCAL3,
84    /// Reserved for local use.
85    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL4"))]
86    Local4 = libc::LOG_LOCAL4,
87    /// Reserved for local use.
88    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL5"))]
89    Local5 = libc::LOG_LOCAL5,
90    /// Reserved for local use.
91    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL6"))]
92    Local6 = libc::LOG_LOCAL6,
93    /// Reserved for local use.
94    #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL7"))]
95    Local7 = libc::LOG_LOCAL7,
96}
97
98impl Default for Facility {
99    fn default() -> Self {
100        Self::User
101    }
102}
103
104/// `syslog` severity.
105#[derive(Copy, Clone)]
106#[repr(i32)]
107// There are more `syslog` severities than `tracing` levels, so some severities
108// aren't used. They're included here for completeness and so the level mapping
109// could easily change to include them.
110#[allow(dead_code)]
111enum Severity {
112    /// System is unusable.
113    #[cfg_attr(docsrs, doc(alias = "LOG_EMERG"))]
114    Emergency = libc::LOG_EMERG,
115    /// Action must be taken immediately.
116    #[cfg_attr(docsrs, doc(alias = "LOG_ALERT"))]
117    Alert = libc::LOG_ALERT,
118    /// Critical conditions.
119    #[cfg_attr(docsrs, doc(alias = "LOG_CRIT"))]
120    Critical = libc::LOG_CRIT,
121    /// Error conditions.
122    #[cfg_attr(docsrs, doc(alias = "LOG_ERR"))]
123    Error = libc::LOG_ERR,
124    /// Warning conditions.
125    #[cfg_attr(docsrs, doc(alias = "LOG_WARNING"))]
126    Warning = libc::LOG_WARNING,
127    /// Normal, but significant, condition.
128    #[cfg_attr(docsrs, doc(alias = "LOG_NOTICE"))]
129    Notice = libc::LOG_NOTICE,
130    /// Informational message.
131    #[cfg_attr(docsrs, doc(alias = "LOG_INFO"))]
132    Info = libc::LOG_INFO,
133    /// Debug-level message.
134    #[cfg_attr(docsrs, doc(alias = "LOG_DEBUG"))]
135    Debug = libc::LOG_DEBUG,
136}
137
138impl From<Level> for Severity {
139    fn from(level: Level) -> Self {
140        match level {
141            Level::ERROR => Self::Error,
142            Level::WARN => Self::Warning,
143            Level::INFO => Self::Notice,
144            Level::DEBUG => Self::Info,
145            Level::TRACE => Self::Debug,
146        }
147    }
148}
149
150/// `syslog` priority.
151#[derive(Copy, Clone, Debug)]
152struct Priority(libc::c_int);
153
154impl Priority {
155    fn new(facility: Facility, level: Level) -> Self {
156        let severity = Severity::from(level);
157        Self((facility as libc::c_int) | (severity as libc::c_int))
158    }
159}
160
161/// What to do when a log message contains characters that are invalid in C strings.
162#[derive(Copy, Clone)]
163pub enum InvalidCharAction {
164    /// Replace invalid characters with the provided character and try again
165    ReplaceWith(char),
166    /// Remove invalid characters and try again
167    Remove,
168    /// Print a warning to stderr and do not log to syslog
169    Warn,
170    /// Panic
171    Panic,
172}
173
174impl Default for InvalidCharAction {
175    fn default() -> Self {
176        Self::ReplaceWith(char::REPLACEMENT_CHARACTER)
177    }
178}
179
180fn syslog(priority: Priority, msg: &CStr) {
181    // SAFETY: the second argument must be a valid pointer to a nul-terminated
182    // format string and formatting placeholders e.g. %s must correspond to
183    // one of the variable-length arguments. By construction, the format string
184    // is nul-terminated, and the only string formatting placeholder corresponds
185    // to `msg.as_ptr()`, which is a valid, nul-terminated string in C world
186    // because `msg` is a `CStr`.
187    unsafe { libc::syslog(priority.0, "%s\0".as_ptr().cast(), msg.as_ptr()) }
188}
189
190/// [`MakeWriter`] that logs to `syslog` via `libc`'s [`syslog()`](libc::syslog) function.
191///
192/// # Level Mapping
193///
194/// `tracing` [`Level`]s are mapped to `syslog` severities as follows:
195///
196/// ```raw
197/// Level::ERROR => Severity::LOG_ERR,
198/// Level::WARN  => Severity::LOG_WARNING,
199/// Level::INFO  => Severity::LOG_NOTICE,
200/// Level::DEBUG => Severity::LOG_INFO,
201/// Level::TRACE => Severity::LOG_DEBUG,
202/// ```
203///
204/// **Note:** the mapping is lossless, but the corresponding `syslog` severity
205/// names differ from `tracing`'s level names towards the bottom. `syslog`
206/// does not have a level lower than `LOG_DEBUG`, so this is unavoidable.
207///
208/// # Examples
209///
210/// Initializing a global logger that writes to `syslog` with an identity of `example-program`
211/// and the default `syslog` options and facility:
212///
213/// ```
214/// let identity = std::ffi::CStr::from_bytes_with_nul(b"example-program\0").unwrap();
215/// let (options, facility) = Default::default();
216/// let syslog = syslog_tracing::Syslog::new(identity, options, facility).unwrap();
217/// tracing_subscriber::fmt().with_writer(syslog).init();
218/// ```
219pub struct Syslog {
220    /// Identity e.g. program name. Referenced by syslog, so we store it here to
221    /// ensure it lives until we are done logging.
222    #[allow(dead_code)]
223    identity: Cow<'static, CStr>,
224    facility: Facility,
225    invalid_chars: InvalidCharAction,
226}
227
228impl Syslog {
229    /// Tracks whether there is a logger currently initialized (i.e. whether there
230    /// has been an `openlog()` call without a corresponding `closelog()` call).
231    fn initialized() -> &'static AtomicBool {
232        static INITIALIZED: AtomicBool = AtomicBool::new(false);
233        &INITIALIZED
234    }
235
236    /// Creates a [`tracing`] [`MakeWriter`] that writes to `syslog`.
237    ///
238    /// This calls [`libc::openlog()`] to initialize the logger. The corresponding
239    /// [`libc::closelog()`] call happens when the returned logger is dropped.
240    /// If a logger already exists, returns `None`.
241    ///
242    /// # Examples
243    ///
244    /// Creating a `syslog` [`MakeWriter`] with an identity of `example-program` and
245    /// the default `syslog` options and facility:
246    ///
247    /// ```
248    /// use syslog_tracing::Syslog;
249    /// let identity = std::ffi::CStr::from_bytes_with_nul(b"example-program\0").unwrap();
250    /// let (options, facility) = Default::default();
251    /// let syslog = Syslog::new(identity, options, facility).unwrap();
252    /// ```
253    ///
254    /// Two loggers cannot coexist, since [`libc::syslog()`] writes to a global logger:
255    ///
256    /// ```
257    /// # use syslog_tracing::Syslog;
258    /// # let identity = std::ffi::CStr::from_bytes_with_nul(b"example-program\0").unwrap();
259    /// # let (options, facility) = Default::default();
260    /// let syslog = Syslog::new(identity, options, facility).unwrap();
261    /// assert!(Syslog::new(identity, options, facility).is_none());
262    /// ```
263    pub fn new(
264        identity: impl Into<Cow<'static, CStr>>,
265        options: Options,
266        facility: Facility,
267    ) -> Option<Self> {
268        use std::sync::atomic::Ordering;
269        // Make sure another logger isn't already initialized
270        if let Ok(false) =
271            Self::initialized().compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
272        {
273            let identity = identity.into();
274            // SAFETY: identity will remain alive until the returned struct's fields
275            // are dropped, by which point `closelog` will have been called by the
276            // `Drop` implementation.
277            unsafe { libc::openlog(identity.as_ptr(), options.0, facility as libc::c_int) };
278            Some(Syslog {
279                identity,
280                facility,
281                invalid_chars: Default::default(),
282            })
283        } else {
284            None
285        }
286    }
287
288    /// Set the action to take when a message cannot be logged as is because it contains invalid characters.
289    pub fn invalid_chars(mut self, cfg: InvalidCharAction) -> Self {
290        self.invalid_chars = cfg;
291        self
292    }
293
294    fn writer(&self, level: Level) -> SyslogWriter {
295        SyslogWriter {
296            flushed: false,
297            facility: self.facility,
298            level,
299            invalid_chars: self.invalid_chars,
300        }
301    }
302}
303
304impl Drop for Syslog {
305    /// Calls [`libc::closelog()`].
306    fn drop(&mut self) {
307        unsafe { libc::closelog() };
308
309        // Since only one logger can be initialized at a time (enforced by the
310        // constructor), dropping a logger means there is now no initialized
311        // logger.
312        use std::sync::atomic::Ordering;
313        assert!(Self::initialized().swap(false, Ordering::SeqCst));
314    }
315}
316
317impl<'a> MakeWriter<'a> for Syslog {
318    type Writer = SyslogWriter;
319
320    fn make_writer(&'a self) -> Self::Writer {
321        self.writer(Level::INFO)
322    }
323
324    fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
325        self.writer(*meta.level())
326    }
327}
328
329/// [Writer](io::Write) to `syslog` produced by [`MakeWriter`].
330pub struct SyslogWriter {
331    flushed: bool,
332    facility: Facility,
333    level: Level,
334    invalid_chars: InvalidCharAction,
335}
336
337thread_local! { static BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(256)) }
338
339impl io::Write for SyslogWriter {
340    fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
341        BUF.with(|buf| buf.borrow_mut().extend(bytes));
342        self.flushed = false;
343        Ok(bytes.len())
344    }
345
346    fn flush(&mut self) -> io::Result<()> {
347        BUF.with(|buf| {
348            let mut buf = buf.borrow_mut();
349
350            // Append nul-terminator
351            buf.push(0);
352
353            let priority = Priority::new(self.facility, self.level);
354
355            // Send the message to `syslog` if the message is valid
356            match CStr::from_bytes_with_nul(&buf) {
357                Ok(msg) => syslog(priority, msg),
358                Err(_) => {
359                    // Since we push a nul byte to `buf` above, it must be that `buf` contained an
360                    // interior nul byte
361                    match self.invalid_chars {
362                        InvalidCharAction::Remove => {
363                            buf.retain(|&c| c != 0);
364                            buf.push(0);
365                            let msg = CStr::from_bytes_with_nul(&buf).unwrap();
366                            syslog(priority, msg);
367                        }
368                        InvalidCharAction::ReplaceWith(c) => {
369                            let mut replacement_bytes = [0; 4];
370                            let replacement_bytes = c.encode_utf8(&mut replacement_bytes).as_bytes();
371                            let mut msg = vec![];
372                            for &c in &buf[..buf.len()-1] {
373                                match c {
374                                    0 => msg.extend_from_slice(replacement_bytes),
375                                    c => msg.push(c),
376                                }
377                            }
378                            msg.push(0);
379                            let msg = CStr::from_bytes_with_nul(&msg).unwrap();
380                            syslog(priority, msg);
381                        }
382                        InvalidCharAction::Warn => {
383                            let buf = buf.as_slice();
384                            let utf8 = str::from_utf8(buf);
385                            let debug: &dyn fmt::Debug = match utf8 {
386                                Ok(ref str) => str,
387                                Err(_) => &buf,
388                            };
389                            eprintln!("syslog-tracing: message to be logged contained interior nul byte: {debug:?}");
390                        }
391                        InvalidCharAction::Panic => {
392                            let buf = buf.as_slice();
393                            let utf8 = str::from_utf8(buf);
394                            let debug: &dyn fmt::Debug = match utf8 {
395                                Ok(ref str) => str,
396                                Err(_) => &buf,
397                            };
398                            panic!("syslog-tracing: message to be logged contained interior nul byte: {debug:?}");
399                        }
400                    }
401                }
402            }
403
404            // Clear buffer
405            buf.clear();
406
407            self.flushed = true;
408            Ok(())
409        })
410    }
411}
412
413impl Drop for SyslogWriter {
414    fn drop(&mut self) {
415        if !self.flushed {
416            let _ = io::Write::flush(self);
417        }
418    }
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424    use once_cell::sync::Lazy;
425    use std::sync::Mutex;
426
427    const IDENTITY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"example-program\0") };
428    const OPTIONS: Options = Options(0);
429    const FACILITY: Facility = Facility::User;
430
431    static INITIALIZED: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
432
433    fn capture_stderr(f: impl FnOnce()) -> String {
434        use std::io::Read;
435        let mut buf = gag::BufferRedirect::stderr().unwrap();
436        f();
437        let mut output = String::new();
438        buf.read_to_string(&mut output).unwrap();
439        output
440    }
441
442    fn with_initialized(configure: impl FnOnce(Syslog) -> Syslog, f: impl FnOnce()) -> Vec<String> {
443        let _lock = INITIALIZED.lock();
444        let syslog = Syslog::new(IDENTITY, OPTIONS | Options::LOG_PERROR, FACILITY).unwrap();
445        let subscriber = tracing_subscriber::fmt()
446            .with_writer(configure(syslog))
447            .finish();
448        tracing::subscriber::with_default(subscriber, || capture_stderr(f))
449            .lines()
450            .map(String::from)
451            .collect()
452    }
453
454    #[test]
455    fn double_init() {
456        let _lock = INITIALIZED.lock();
457        let _syslog = Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
458        assert!(
459            Syslog::new(IDENTITY, OPTIONS, FACILITY).is_none(),
460            "double initialization"
461        );
462    }
463
464    #[test]
465    fn init_after_drop() {
466        let _lock = INITIALIZED.lock();
467        let syslog = Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
468        drop(syslog);
469        Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
470    }
471
472    #[test]
473    fn basic_log() {
474        let text = "test message";
475        match with_initialized(|syslog| syslog, || tracing::info!("{}", text)).as_slice() {
476            [msg] if msg.contains(text) => (),
477            x => panic!("expected log message containing '{}', got '{:?}'", text, x),
478        }
479    }
480
481    #[test]
482    fn write_after_flush() {
483        let _lock = INITIALIZED.lock();
484
485        let process = "example-program";
486        let text = "test message";
487
488        let msg = capture_stderr(|| {
489            use std::io::Write;
490
491            let syslog = Syslog::new(IDENTITY, OPTIONS | Options::LOG_PERROR, FACILITY).unwrap();
492            let mut writer = syslog.make_writer();
493
494            writer.write_all(text.as_bytes()).unwrap();
495            writer.flush().unwrap();
496
497            writer.write_all(text.as_bytes()).unwrap();
498            // writer dropped here -> flush()
499        });
500
501        assert_eq!(msg, format!("{process}: {text}\n{process}: {text}\n"))
502    }
503
504    #[test]
505    #[should_panic = "interior nul byte"]
506    fn invalid_chars_panic() {
507        with_initialized(
508            |syslog| syslog.invalid_chars(InvalidCharAction::Panic),
509            || tracing::info!("before\0after"),
510        );
511    }
512
513    #[test]
514    fn invalid_chars_warn() {
515        match with_initialized(
516            |syslog| syslog.invalid_chars(InvalidCharAction::Warn),
517            || tracing::info!("before\0after"),
518        )
519        .as_slice()
520        {
521            [msg] => assert!(msg.contains("interior nul byte")),
522            x => panic!("unexpected output: {x:?}"),
523        }
524    }
525
526    #[test]
527    fn invalid_chars_remove() {
528        match with_initialized(
529            |syslog| syslog.invalid_chars(InvalidCharAction::Remove),
530            || tracing::info!("before\0after"),
531        )
532        .as_slice()
533        {
534            [msg] => assert!(msg.contains("beforeafter")),
535            x => panic!("unexpected output: {x:?}"),
536        }
537    }
538
539    #[test]
540    fn invalid_chars_replace() {
541        match with_initialized(
542            |syslog| {
543                syslog.invalid_chars(InvalidCharAction::ReplaceWith(char::REPLACEMENT_CHARACTER))
544            },
545            || tracing::info!("before\0after"),
546        )
547        .as_slice()
548        {
549            [msg] => {
550                assert!(msg.contains(&format!("before{}after", char::REPLACEMENT_CHARACTER)))
551            }
552            x => panic!("unexpected output: {x:?}"),
553        }
554    }
555}