use std::{borrow::Cow, cell::RefCell, ffi::CStr, fmt, io, str, sync::atomic::AtomicBool};
use tracing_core::{Level, Metadata};
use tracing_subscriber::fmt::MakeWriter;
#[derive(Copy, Clone, Debug, Default)]
pub struct Options(libc::c_int);
impl Options {
pub const LOG_PID: Self = Self(libc::LOG_PID);
pub const LOG_CONS: Self = Self(libc::LOG_CONS);
pub const LOG_ODELAY: Self = Self(libc::LOG_ODELAY);
pub const LOG_NDELAY: Self = Self(libc::LOG_NDELAY);
pub const LOG_NOWAIT: Self = Self(libc::LOG_NOWAIT);
pub const LOG_PERROR: Self = Self(libc::LOG_PERROR);
}
impl std::ops::BitOr for Options {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
#[derive(Copy, Clone, Debug)]
#[repr(i32)]
pub enum Facility {
#[cfg_attr(docsrs, doc(alias = "LOG_USER"))]
User = libc::LOG_USER,
#[cfg_attr(docsrs, doc(alias = "LOG_MAIL"))]
Mail = libc::LOG_MAIL,
#[cfg_attr(docsrs, doc(alias = "LOG_DAEMON"))]
Daemon = libc::LOG_DAEMON,
#[cfg_attr(docsrs, doc(alias = "LOG_AUTH"))]
Auth = libc::LOG_AUTH,
#[cfg_attr(docsrs, doc(alias = "LOG_LPR"))]
Lpr = libc::LOG_LPR,
#[cfg_attr(docsrs, doc(alias = "LOG_NEWS"))]
News = libc::LOG_NEWS,
#[cfg_attr(docsrs, doc(alias = "LOG_UUCP"))]
Uucp = libc::LOG_UUCP,
#[cfg_attr(docsrs, doc(alias = "LOG_CRON"))]
Cron = libc::LOG_CRON,
#[cfg_attr(docsrs, doc(alias = "LOG_AUTHPRIV"))]
AuthPriv = libc::LOG_AUTHPRIV,
#[cfg_attr(docsrs, doc(alias = "LOG_FTP"))]
Ftp = libc::LOG_FTP,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL0"))]
Local0 = libc::LOG_LOCAL0,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL1"))]
Local1 = libc::LOG_LOCAL1,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL2"))]
Local2 = libc::LOG_LOCAL2,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL3"))]
Local3 = libc::LOG_LOCAL3,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL4"))]
Local4 = libc::LOG_LOCAL4,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL5"))]
Local5 = libc::LOG_LOCAL5,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL6"))]
Local6 = libc::LOG_LOCAL6,
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL7"))]
Local7 = libc::LOG_LOCAL7,
}
impl Default for Facility {
fn default() -> Self {
Self::User
}
}
#[derive(Copy, Clone)]
#[repr(i32)]
#[allow(dead_code)]
enum Severity {
#[cfg_attr(docsrs, doc(alias = "LOG_EMERG"))]
Emergency = libc::LOG_EMERG,
#[cfg_attr(docsrs, doc(alias = "LOG_ALERT"))]
Alert = libc::LOG_ALERT,
#[cfg_attr(docsrs, doc(alias = "LOG_CRIT"))]
Critical = libc::LOG_CRIT,
#[cfg_attr(docsrs, doc(alias = "LOG_ERR"))]
Error = libc::LOG_ERR,
#[cfg_attr(docsrs, doc(alias = "LOG_WARNING"))]
Warning = libc::LOG_WARNING,
#[cfg_attr(docsrs, doc(alias = "LOG_NOTICE"))]
Notice = libc::LOG_NOTICE,
#[cfg_attr(docsrs, doc(alias = "LOG_INFO"))]
Info = libc::LOG_INFO,
#[cfg_attr(docsrs, doc(alias = "LOG_DEBUG"))]
Debug = libc::LOG_DEBUG,
}
impl From<Level> for Severity {
fn from(level: Level) -> Self {
match level {
Level::ERROR => Self::Error,
Level::WARN => Self::Warning,
Level::INFO => Self::Notice,
Level::DEBUG => Self::Info,
Level::TRACE => Self::Debug,
}
}
}
#[derive(Copy, Clone, Debug)]
struct Priority(libc::c_int);
impl Priority {
fn new(facility: Facility, level: Level) -> Self {
let severity = Severity::from(level);
Self((facility as libc::c_int) | (severity as libc::c_int))
}
}
#[derive(Copy, Clone)]
pub enum InvalidCharAction {
ReplaceWith(char),
Remove,
Warn,
Panic,
}
impl Default for InvalidCharAction {
fn default() -> Self {
Self::ReplaceWith(char::REPLACEMENT_CHARACTER)
}
}
fn syslog(priority: Priority, msg: &CStr) {
unsafe { libc::syslog(priority.0, "%s\0".as_ptr().cast(), msg.as_ptr()) }
}
pub struct Syslog {
#[allow(dead_code)]
identity: Cow<'static, CStr>,
facility: Facility,
invalid_chars: InvalidCharAction,
}
impl Syslog {
fn initialized() -> &'static AtomicBool {
static INITIALIZED: AtomicBool = AtomicBool::new(false);
&INITIALIZED
}
pub fn new(
identity: impl Into<Cow<'static, CStr>>,
options: Options,
facility: Facility,
) -> Option<Self> {
use std::sync::atomic::Ordering;
if let Ok(false) =
Self::initialized().compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
{
let identity = identity.into();
unsafe { libc::openlog(identity.as_ptr(), options.0, facility as libc::c_int) };
Some(Syslog {
identity,
facility,
invalid_chars: Default::default(),
})
} else {
None
}
}
pub fn invalid_chars(mut self, cfg: InvalidCharAction) -> Self {
self.invalid_chars = cfg;
self
}
fn writer(&self, level: Level) -> SyslogWriter {
SyslogWriter {
flushed: false,
facility: self.facility,
level,
invalid_chars: self.invalid_chars,
}
}
}
impl Drop for Syslog {
fn drop(&mut self) {
unsafe { libc::closelog() };
use std::sync::atomic::Ordering;
assert!(Self::initialized().swap(false, Ordering::SeqCst));
}
}
impl<'a> MakeWriter<'a> for Syslog {
type Writer = SyslogWriter;
fn make_writer(&'a self) -> Self::Writer {
self.writer(Level::INFO)
}
fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
self.writer(*meta.level())
}
}
pub struct SyslogWriter {
flushed: bool,
facility: Facility,
level: Level,
invalid_chars: InvalidCharAction,
}
thread_local! { static BUF: RefCell<Vec<u8>> = RefCell::new(Vec::with_capacity(256)) }
impl io::Write for SyslogWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
BUF.with(|buf| buf.borrow_mut().extend(bytes));
self.flushed = false;
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
BUF.with(|buf| {
let mut buf = buf.borrow_mut();
buf.push(0);
let priority = Priority::new(self.facility, self.level);
match CStr::from_bytes_with_nul(&buf) {
Ok(msg) => syslog(priority, msg),
Err(_) => {
match self.invalid_chars {
InvalidCharAction::Remove => {
buf.retain(|&c| c != 0);
buf.push(0);
let msg = CStr::from_bytes_with_nul(&buf).unwrap();
syslog(priority, msg);
}
InvalidCharAction::ReplaceWith(c) => {
let mut replacement_bytes = [0; 4];
let replacement_bytes = c.encode_utf8(&mut replacement_bytes).as_bytes();
let mut msg = vec![];
for &c in &buf[..buf.len()-1] {
match c {
0 => msg.extend_from_slice(replacement_bytes),
c => msg.push(c),
}
}
msg.push(0);
let msg = CStr::from_bytes_with_nul(&msg).unwrap();
syslog(priority, msg);
}
InvalidCharAction::Warn => {
let buf = buf.as_slice();
let utf8 = str::from_utf8(buf);
let debug: &dyn fmt::Debug = match utf8 {
Ok(ref str) => str,
Err(_) => &buf,
};
eprintln!("syslog-tracing: message to be logged contained interior nul byte: {debug:?}");
}
InvalidCharAction::Panic => {
let buf = buf.as_slice();
let utf8 = str::from_utf8(buf);
let debug: &dyn fmt::Debug = match utf8 {
Ok(ref str) => str,
Err(_) => &buf,
};
panic!("syslog-tracing: message to be logged contained interior nul byte: {debug:?}");
}
}
}
}
buf.clear();
self.flushed = true;
Ok(())
})
}
}
impl Drop for SyslogWriter {
fn drop(&mut self) {
if !self.flushed {
let _ = io::Write::flush(self);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use once_cell::sync::Lazy;
use std::sync::Mutex;
const IDENTITY: &CStr = unsafe { CStr::from_bytes_with_nul_unchecked(b"example-program\0") };
const OPTIONS: Options = Options(0);
const FACILITY: Facility = Facility::User;
static INITIALIZED: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn capture_stderr(f: impl FnOnce()) -> String {
use std::io::Read;
let mut buf = gag::BufferRedirect::stderr().unwrap();
f();
let mut output = String::new();
buf.read_to_string(&mut output).unwrap();
output
}
fn with_initialized(configure: impl FnOnce(Syslog) -> Syslog, f: impl FnOnce()) -> Vec<String> {
let _lock = INITIALIZED.lock();
let syslog = Syslog::new(IDENTITY, OPTIONS | Options::LOG_PERROR, FACILITY).unwrap();
let subscriber = tracing_subscriber::fmt()
.with_writer(configure(syslog))
.finish();
tracing::subscriber::with_default(subscriber, || capture_stderr(f))
.lines()
.map(String::from)
.collect()
}
#[test]
fn double_init() {
let _lock = INITIALIZED.lock();
let _syslog = Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
assert!(
Syslog::new(IDENTITY, OPTIONS, FACILITY).is_none(),
"double initialization"
);
}
#[test]
fn init_after_drop() {
let _lock = INITIALIZED.lock();
let syslog = Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
drop(syslog);
Syslog::new(IDENTITY, OPTIONS, FACILITY).unwrap();
}
#[test]
fn basic_log() {
let text = "test message";
match with_initialized(|syslog| syslog, || tracing::info!("{}", text)).as_slice() {
[msg] if msg.contains(text) => (),
x => panic!("expected log message containing '{}', got '{:?}'", text, x),
}
}
#[test]
fn write_after_flush() {
let _lock = INITIALIZED.lock();
let process = "example-program";
let text = "test message";
let msg = capture_stderr(|| {
use std::io::Write;
let syslog = Syslog::new(IDENTITY, OPTIONS | Options::LOG_PERROR, FACILITY).unwrap();
let mut writer = syslog.make_writer();
writer.write_all(text.as_bytes()).unwrap();
writer.flush().unwrap();
writer.write_all(text.as_bytes()).unwrap();
});
assert_eq!(msg, format!("{process}: {text}\n{process}: {text}\n"))
}
#[test]
#[should_panic = "interior nul byte"]
fn invalid_chars_panic() {
with_initialized(
|syslog| syslog.invalid_chars(InvalidCharAction::Panic),
|| tracing::info!("before\0after"),
);
}
#[test]
fn invalid_chars_warn() {
match with_initialized(
|syslog| syslog.invalid_chars(InvalidCharAction::Warn),
|| tracing::info!("before\0after"),
)
.as_slice()
{
[msg] => assert!(msg.contains("interior nul byte")),
x => panic!("unexpected output: {x:?}"),
}
}
#[test]
fn invalid_chars_remove() {
match with_initialized(
|syslog| syslog.invalid_chars(InvalidCharAction::Remove),
|| tracing::info!("before\0after"),
)
.as_slice()
{
[msg] => assert!(msg.contains("beforeafter")),
x => panic!("unexpected output: {x:?}"),
}
}
#[test]
fn invalid_chars_replace() {
match with_initialized(
|syslog| {
syslog.invalid_chars(InvalidCharAction::ReplaceWith(char::REPLACEMENT_CHARACTER))
},
|| tracing::info!("before\0after"),
)
.as_slice()
{
[msg] => {
assert!(msg.contains(&format!("before{}after", char::REPLACEMENT_CHARACTER)))
}
x => panic!("unexpected output: {x:?}"),
}
}
}