Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ members = [
"tracing-serde",
"tracing-appender",
"tracing-journald",
"tracing-syslog",
"examples"
]
26 changes: 26 additions & 0 deletions tracing-syslog/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
[package]
name = "tracing-syslog"
version = "0.1.0"
authors = ["Max Heller <max.a.heller@gmail.com>"]
edition = "2018"
license = "MIT"
repository = "https://github.com/tokio-rs/tracing"
homepage = "https://tokio.rs"
description = "syslog subscriber for `tracing`"
categories = [
"development-tools::debugging",
"development-tools::profiling",
]
keywords = ["tracing", "syslog"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[dependencies]
libc = "0.2"
tracing-core = { path = "../tracing-core", version = "0.2" }
tracing-subscriber = { path = "../tracing-subscriber", version = "0.3" }

[dev-dependencies]
tracing = { path = "../tracing", version = "0.2" }
9 changes: 9 additions & 0 deletions tracing-syslog/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//! `syslog` logging support for `tracing` backed by `libc`'s
//! [`syslog()`](libc::syslog) function.
//!
//! See [`Syslog`] for documentation and examples.

#[cfg(unix)]
mod syslog;
#[cfg(unix)]
pub use syslog::*;
Comment on lines +6 to +9
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be great to have doc comments with examples and tests here.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've never been sure about this: for a crate that has a primary type (like Syslog in this case), should the majority of the docs go with that type or in lib.rs?

321 changes: 321 additions & 0 deletions tracing-syslog/src/syslog.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
use std::{borrow::Cow, cell::RefCell, ffi::CStr, io};
use tracing_core::{Level, Metadata};
use tracing_subscriber::fmt::MakeWriter;

/// `syslog` options.
///
/// # Examples
/// ```
/// use tracing_syslog::Options;
/// // Log PID with messages and log to stderr as well as `syslog`.
/// let opts = Options::LOG_PID | Options::LOG_PERROR;
/// ```
#[derive(Copy, Clone, Debug, Default)]
pub struct Options(libc::c_int);

impl Options {
/// Log the pid with each message.
pub const LOG_PID: Self = Self(libc::LOG_PID);
/// Log on the console if errors in sending.
pub const LOG_CONS: Self = Self(libc::LOG_CONS);
/// Delay open until first syslog() (default).
pub const LOG_ODELAY: Self = Self(libc::LOG_ODELAY);
/// Don't delay open.
pub const LOG_NDELAY: Self = Self(libc::LOG_NDELAY);
/// Don't wait for console forks: DEPRECATED.
pub const LOG_NOWAIT: Self = Self(libc::LOG_NOWAIT);
/// Log to stderr as well.
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)
}
}
Comment on lines +31 to +36
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, not sure if this carries its weight. Why do you think this is valuable?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So you can set multiple options at the same time:

use tracing_syslog::Options;
// Log PID with messages and log to stderr as well as `syslog`.
let opts = Options::LOG_PID | Options::LOG_PERROR;

(added this example to Option's docs)


/// `syslog` facility.
#[derive(Copy, Clone, Debug)]
#[repr(i32)]
pub enum Facility {
/// Generic user-level messages.
#[cfg_attr(docsrs, doc(alias = "LOG_USER"))]
User = libc::LOG_USER,
/// Mail subsystem.
#[cfg_attr(docsrs, doc(alias = "LOG_MAIL"))]
Mail = libc::LOG_MAIL,
/// System daemons without separate facility value.
#[cfg_attr(docsrs, doc(alias = "LOG_DAEMON"))]
Daemon = libc::LOG_DAEMON,
/// Security/authorization messages.
#[cfg_attr(docsrs, doc(alias = "LOG_AUTH"))]
Auth = libc::LOG_AUTH,
/// Line printer subsystem.
#[cfg_attr(docsrs, doc(alias = "LOG_LPR"))]
Lpr = libc::LOG_LPR,
/// USENET news subsystem.
#[cfg_attr(docsrs, doc(alias = "LOG_NEWS"))]
News = libc::LOG_NEWS,
/// UUCP subsystem.
#[cfg_attr(docsrs, doc(alias = "LOG_UUCP"))]
Uucp = libc::LOG_UUCP,
/// Clock daemon (`cron` and `at`).
#[cfg_attr(docsrs, doc(alias = "LOG_CRON"))]
Cron = libc::LOG_CRON,
/// Security/authorization messages (private).
#[cfg_attr(docsrs, doc(alias = "LOG_AUTHPRIV"))]
AuthPriv = libc::LOG_AUTHPRIV,
/// FTP daemon.
#[cfg_attr(docsrs, doc(alias = "LOG_FTP"))]
Ftp = libc::LOG_FTP,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL0"))]
Local0 = libc::LOG_LOCAL0,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL1"))]
Local1 = libc::LOG_LOCAL1,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL2"))]
Local2 = libc::LOG_LOCAL2,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL3"))]
Local3 = libc::LOG_LOCAL3,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL4"))]
Local4 = libc::LOG_LOCAL4,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL5"))]
Local5 = libc::LOG_LOCAL5,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL6"))]
Local6 = libc::LOG_LOCAL6,
/// Reserved for local use.
#[cfg_attr(docsrs, doc(alias = "LOG_LOCAL7"))]
Local7 = libc::LOG_LOCAL7,
}

impl Default for Facility {
fn default() -> Self {
Self::User
}
}

/// `syslog` severity.
#[derive(Copy, Clone)]
#[repr(i32)]
// There are more `syslog` severities than `tracing` levels, so some severities
// aren't used. They're included here for completeness and so the level mapping
// could easily change to include them.
#[allow(dead_code)]
enum Severity {
/// System is unusable.
#[cfg_attr(docsrs, doc(alias = "LOG_EMERG"))]
Emergency = libc::LOG_EMERG,
/// Action must be taken immediately.
#[cfg_attr(docsrs, doc(alias = "LOG_ALERT"))]
Alert = libc::LOG_ALERT,
/// Critical conditions.
#[cfg_attr(docsrs, doc(alias = "LOG_CRIT"))]
Critical = libc::LOG_CRIT,
/// Error conditions.
#[cfg_attr(docsrs, doc(alias = "LOG_ERR"))]
Error = libc::LOG_ERR,
/// Warning conditions.
#[cfg_attr(docsrs, doc(alias = "LOG_WARNING"))]
Warning = libc::LOG_WARNING,
/// Normal, but significant, condition.
#[cfg_attr(docsrs, doc(alias = "LOG_NOTICE"))]
Notice = libc::LOG_NOTICE,
/// Informational message.
#[cfg_attr(docsrs, doc(alias = "LOG_INFO"))]
Info = libc::LOG_INFO,
/// Debug-level message.
#[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,
}
}
}

/// `syslog` priority.
#[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))
}
}

fn syslog(priority: Priority, msg: &CStr) {
// SAFETY: the second argument must be a valid pointer to a nul-terminated
// format string and formatting placeholders e.g. %s must correspond to
// one of the variable-length arguments. By construction, the format string
// is nul-terminated, and the only string formatting placeholder corresponds
// to `msg.as_ptr()`, which is a valid, nul-terminated string in C world
// because `msg` is a `CStr`.
unsafe { libc::syslog(priority.0, "%s\0".as_ptr().cast(), msg.as_ptr()) }
}

/// [`MakeWriter`] that logs to `syslog` via `libc`'s [`syslog()`](libc::syslog) function.
///
/// # Level Mapping
///
/// `tracing` [`Level`]s are mapped to `syslog` severities as follows:
///
/// ```raw
/// Level::ERROR => Severity::LOG_ERR,
/// Level::WARN => Severity::LOG_WARNING,
/// Level::INFO => Severity::LOG_NOTICE,
/// Level::DEBUG => Severity::LOG_INFO,
/// Level::TRACE => Severity::LOG_DEBUG,
/// ```
///
/// **Note:** the mapping is lossless, but the corresponding `syslog` severity
/// names differ from `tracing`'s level names towards the bottom. `syslog`
/// does not have a level lower than `LOG_DEBUG`, so this is unavoidable.
///
/// # Examples
///
/// Initializing a global logger that writes to `syslog` with an identity of `example-program`
/// and the default `syslog` options and facility:
///
/// ```
/// let identity = std::ffi::CStr::from_bytes_with_nul(b"example-program\0").unwrap();
/// let (options, facility) = Default::default();
/// let syslog = tracing_syslog::Syslog::new(identity, options, facility);
/// tracing_subscriber::fmt().with_writer(syslog).init();
/// ```
pub struct Syslog {
/// Identity e.g. program name. Referenced by syslog, so we store it here to
/// ensure it lives until we are done logging.
#[allow(dead_code)]
identity: Cow<'static, CStr>,
facility: Facility,
}

impl Syslog {
/// Creates a [`MakeWriter`] that writes to `syslog`.
///
/// This calls [`libc::openlog()`] to initialize the logger. The corresponding
/// [`libc::closelog()`] call happens when the returned logger is dropped.
///
/// # Examples
///
/// Creating a `syslog` subscriber with an identity of `example-program` and
/// the default `syslog` options and facility:
///
/// ```
/// use tracing_syslog::Syslog;
/// let identity = std::ffi::CStr::from_bytes_with_nul(b"example-program\0").unwrap();
/// let (options, facility) = Default::default();
/// let subscriber = Syslog::new(identity, options, facility);
/// ```
pub fn new(
identity: impl Into<Cow<'static, CStr>>,
options: Options,
facility: Facility,
) -> Self {
let identity = identity.into();
// SAFETY: identity will remain alive until the returned struct's fields
// are dropped, by which point `closelog` will have been called by the
// `Drop` implementation.
unsafe { libc::openlog(identity.as_ptr(), options.0, facility as libc::c_int) };
Syslog { identity, facility }
}
}

impl Drop for Syslog {
/// Calls [`libc::closelog()`].
fn drop(&mut self) {
unsafe { libc::closelog() };
}
Comment on lines +241 to +243
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why's this safe? What invarients need to be upheld? If you can, can you add comments explaining why?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't decided yet how to best handle the case where multiple Syslogs are created (and thus openlog() is called multiple times). Recently created loggers will clobber the identity, facility, and options of previously created loggers, but what's worse is that when one is dropped and calls closelog() it will destroy the identity, facility, and options of the other ones. For these reasons, I think it might be best to have an AtomicBool or similar to keep track of whether or not a logger is currently initialized, and panic if a user tries to initialize multiple concurrently.

}

impl<'a> MakeWriter<'a> for Syslog {
type Writer = SyslogWriter;

fn make_writer(&'a self) -> Self::Writer {
// TODO: is `INFO` a good default?
SyslogWriter::new(self.facility, Level::INFO)
}

fn make_writer_for(&'a self, meta: &Metadata<'_>) -> Self::Writer {
SyslogWriter::new(self.facility, *meta.level())
}
}

/// [Writer](io::Write) to `syslog` produced by [`MakeWriter`].
pub struct SyslogWriter {
flushed: bool,
facility: Facility,
level: Level,
}

impl SyslogWriter {
fn new(facility: Facility, level: Level) -> Self {
SyslogWriter {
flushed: false,
facility,
level,
}
}
}

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));
Ok(bytes.len())
}

fn flush(&mut self) -> io::Result<()> {
BUF.with(|buf| {
let mut buf = buf.borrow_mut();

// Append nul-terminator
buf.push(0);

// Parse log message as C string
let msg = CStr::from_bytes_with_nul(&buf);

// In debug mode, panic when log message contains interior nul
#[cfg(debug_assertions)]
msg.as_ref().expect("logs free of interior nul-terminators");
// ... but in non-debug mode, just print an error
#[cfg(not(debug_assertions))]
let msg = msg.map_err(|res| eprintln!("log contained interior nul byte: {}", res));

// Send the message to `syslog` if the message is valid
if let Ok(msg) = msg {
let priority = Priority::new(self.facility, self.level);
syslog(priority, msg)
}

// Clear buffer
buf.clear();
});
self.flushed = true;
Ok(())
}
}

impl Drop for SyslogWriter {
fn drop(&mut self) {
if !self.flushed {
let _ = io::Write::flush(self);
}
}
}