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#[derive(Copy, Clone, Debug, Default)]
14pub struct Options(libc::c_int);
15
16impl Options {
17 pub const LOG_PID: Self = Self(libc::LOG_PID);
19 pub const LOG_CONS: Self = Self(libc::LOG_CONS);
21 pub const LOG_ODELAY: Self = Self(libc::LOG_ODELAY);
23 pub const LOG_NDELAY: Self = Self(libc::LOG_NDELAY);
25 pub const LOG_NOWAIT: Self = Self(libc::LOG_NOWAIT);
27 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#[derive(Copy, Clone, Debug)]
40#[repr(i32)]
41pub enum Facility {
42 #[cfg_attr(docsrs, doc(alias = "LOG_USER"))]
44 User = libc::LOG_USER,
45 #[cfg_attr(docsrs, doc(alias = "LOG_MAIL"))]
47 Mail = libc::LOG_MAIL,
48 #[cfg_attr(docsrs, doc(alias = "LOG_DAEMON"))]
50 Daemon = libc::LOG_DAEMON,
51 #[cfg_attr(docsrs, doc(alias = "LOG_AUTH"))]
53 Auth = libc::LOG_AUTH,
54 #[cfg_attr(docsrs, doc(alias = "LOG_LPR"))]
56 Lpr = libc::LOG_LPR,
57 #[cfg_attr(docsrs, doc(alias = "LOG_NEWS"))]
59 News = libc::LOG_NEWS,
60 #[cfg_attr(docsrs, doc(alias = "LOG_UUCP"))]
62 Uucp = libc::LOG_UUCP,
63 #[cfg_attr(docsrs, doc(alias = "LOG_CRON"))]
65 Cron = libc::LOG_CRON,
66 #[cfg_attr(docsrs, doc(alias = "LOG_AUTHPRIV"))]
68 AuthPriv = libc::LOG_AUTHPRIV,
69 #[cfg_attr(docsrs, doc(alias = "LOG_FTP"))]
71 Ftp = libc::LOG_FTP,
72 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL0"))]
74 Local0 = libc::LOG_LOCAL0,
75 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL1"))]
77 Local1 = libc::LOG_LOCAL1,
78 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL2"))]
80 Local2 = libc::LOG_LOCAL2,
81 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL3"))]
83 Local3 = libc::LOG_LOCAL3,
84 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL4"))]
86 Local4 = libc::LOG_LOCAL4,
87 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL5"))]
89 Local5 = libc::LOG_LOCAL5,
90 #[cfg_attr(docsrs, doc(alias = "LOG_LOCAL6"))]
92 Local6 = libc::LOG_LOCAL6,
93 #[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#[derive(Copy, Clone)]
106#[repr(i32)]
107#[allow(dead_code)]
111enum Severity {
112 #[cfg_attr(docsrs, doc(alias = "LOG_EMERG"))]
114 Emergency = libc::LOG_EMERG,
115 #[cfg_attr(docsrs, doc(alias = "LOG_ALERT"))]
117 Alert = libc::LOG_ALERT,
118 #[cfg_attr(docsrs, doc(alias = "LOG_CRIT"))]
120 Critical = libc::LOG_CRIT,
121 #[cfg_attr(docsrs, doc(alias = "LOG_ERR"))]
123 Error = libc::LOG_ERR,
124 #[cfg_attr(docsrs, doc(alias = "LOG_WARNING"))]
126 Warning = libc::LOG_WARNING,
127 #[cfg_attr(docsrs, doc(alias = "LOG_NOTICE"))]
129 Notice = libc::LOG_NOTICE,
130 #[cfg_attr(docsrs, doc(alias = "LOG_INFO"))]
132 Info = libc::LOG_INFO,
133 #[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#[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#[derive(Copy, Clone)]
163pub enum InvalidCharAction {
164 ReplaceWith(char),
166 Remove,
168 Warn,
170 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 unsafe { libc::syslog(priority.0, "%s\0".as_ptr().cast(), msg.as_ptr()) }
188}
189
190pub struct Syslog {
220 #[allow(dead_code)]
223 identity: Cow<'static, CStr>,
224 facility: Facility,
225 invalid_chars: InvalidCharAction,
226}
227
228impl Syslog {
229 fn initialized() -> &'static AtomicBool {
232 static INITIALIZED: AtomicBool = AtomicBool::new(false);
233 &INITIALIZED
234 }
235
236 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 if let Ok(false) =
271 Self::initialized().compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
272 {
273 let identity = identity.into();
274 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 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 fn drop(&mut self) {
307 unsafe { libc::closelog() };
308
309 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
329pub 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 buf.push(0);
352
353 let priority = Priority::new(self.facility, self.level);
354
355 match CStr::from_bytes_with_nul(&buf) {
357 Ok(msg) => syslog(priority, msg),
358 Err(_) => {
359 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 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 });
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}