From 63294d3966b90a1d3fb8017459680e0009f95854 Mon Sep 17 00:00:00 2001 From: argv-minus-one Date: Thu, 18 Jun 2020 19:23:46 -0700 Subject: [PATCH 1/3] Rework using the POSIX syslog API. This is a breaking change. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As discussed in PR #5, this is based on the [syslog backend that I wrote for sloggers][1] and does not use the `syslog` crate. It is now very different from both slog-syslog 0.12 and PR #5. Differences from #5 include: * Uses the POSIX syslog API through the `libc` crate. (See “design and rationale” in src/lib.rs, starting at line 52, for details.) This means: * Only works on Unix-like platforms. * Only sends log messages to the local syslog daemon, not to a remote server. * Formats log messages into a reusable thread-local buffer, as in slog-syslog 0.12. * The `Drain` implementation is now called `SyslogDrain` instead of `Streamer3164`, since local syslog doesn't use the RFC3164 protocol. * If the `serde` feature is enabled, logging settings can be loaded from a configuration file. Minimum supported Rust version is 1.27.2, or if the `serde` feature is enabled, 1.31.0. Doc-tests require 1.28.0. Works (except doc-tests) on 1.26.2 if you run the following commands first: ``` cargo generate-lockfile cargo update --package lazy_static --precise 1.3.0 cargo update --package serde --precise 1.0.58 cargo update --package serde_derive --precise 1.0.58 ``` [1]: https://github.com/sile/sloggers/pull/34 --- Cargo.toml | 18 +- Makefile | 14 +- examples/syslog-unix.rs | 7 +- lib.rs | 279 --------------------- src/builder.rs | 269 ++++++++++++++++++++ src/config.rs | 231 +++++++++++++++++ src/drain.rs | 314 ++++++++++++++++++++++++ src/facility.rs | 532 ++++++++++++++++++++++++++++++++++++++++ src/format.rs | 364 +++++++++++++++++++++++++++ src/lib.rs | 161 ++++++++++++ src/mock.rs | 89 +++++++ src/tests.rs | 98 ++++++++ 12 files changed, 2081 insertions(+), 295 deletions(-) delete mode 100644 lib.rs create mode 100644 src/builder.rs create mode 100644 src/config.rs create mode 100644 src/drain.rs create mode 100644 src/facility.rs create mode 100644 src/format.rs create mode 100644 src/lib.rs create mode 100644 src/mock.rs create mode 100644 src/tests.rs diff --git a/Cargo.toml b/Cargo.toml index 620ea5f..7bb045b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,19 +1,23 @@ [package] name = "slog-syslog" -version = "0.12.0" +version = "0.13.0" authors = ["Dawid Ciężarkiewicz ", "William Laeder /dev/null && ls *.rs 2>/dev/null | sed -e 's/.rs$$//g' ) all: $(ALL_TARGETS) .PHONY: run test build doc clean clippy -run test build clean: +run test build: + cargo $@ --features $(CARGO_FEATURES) $(CARGO_FLAGS) + +clean: cargo $@ $(CARGO_FLAGS) check: $(info Running check; use `make build` to actually build) - cargo $@ $(CARGO_FLAGS) + cargo $@ --features $(CARGO_FEATURES) $(CARGO_FLAGS) clippy: - cargo build --features clippy + cargo build --features clippy,$(CARGO_FEATURES) .PHONY: bench bench: - cargo $@ $(filter-out --release,$(CARGO_FLAGS)) + cargo $@ --features $(CARGO_FEATURES) $(filter-out --release,$(CARGO_FLAGS)) .PHONY: travistest travistest: test @@ -50,7 +54,7 @@ $(EXAMPLES): .PHONY: doc doc: FORCE - cargo doc + cargo --features $(CARGO_FEATURES) doc .PHONY: publishdoc publishdoc: diff --git a/examples/syslog-unix.rs b/examples/syslog-unix.rs index 3a3a2f1..d4bb0d7 100644 --- a/examples/syslog-unix.rs +++ b/examples/syslog-unix.rs @@ -2,12 +2,11 @@ extern crate slog; extern crate slog_syslog; -use slog::Drain; -use slog_syslog::Facility; +use slog_syslog::{Facility, SyslogBuilder}; fn main() { - let syslog = slog_syslog::unix_3164(Facility::LOG_USER).unwrap(); - let root = slog::Logger::root(syslog.fuse(), o!()); + let syslog = SyslogBuilder::new().facility(Facility::User).build(); + let root = slog::Logger::root(syslog, o!()); info!(root, "Starting"); diff --git a/lib.rs b/lib.rs deleted file mode 100644 index 07dfffe..0000000 --- a/lib.rs +++ /dev/null @@ -1,279 +0,0 @@ -//! Syslog drain for slog-rs -//! -//! ``` -//! extern crate slog; -//! extern crate slog_syslog; -//! -//! use slog::*; -//! use slog_syslog::Facility; -//! -//! fn main() { -//! let o = o!("build-id" => "8dfljdf"); -//! -//! // log to a local unix sock `/var/run/syslog` -//! match slog_syslog::SyslogBuilder::new() -//! .facility(Facility::LOG_USER) -//! .level(slog::Level::Debug) -//! .unix("/var/run/syslog") -//! .start() { -//! Ok(x) => { -//! let root = Logger::root(x.fuse(), o); -//! }, -//! Err(e) => println!("Failed to start syslog on `var/run/syslog`. Error {:?}", e) -//! }; -//! } -//! ``` -#![warn(missing_docs)] - -extern crate nix; -extern crate slog; -extern crate syslog; - -use slog::{Drain, Level, OwnedKVList, Record}; -use std::{fmt, io}; -use std::sync::Mutex; -use std::cell::RefCell; -use std::path::{Path, PathBuf}; -use std::net::SocketAddr; -use std::io::{Error, ErrorKind}; - -use slog::KV; - -pub use syslog::Facility; - -thread_local! { - static TL_BUF: RefCell> = RefCell::new(Vec::with_capacity(128)) -} - -fn level_to_severity(level: slog::Level) -> syslog::Severity { - match level { - Level::Critical => syslog::Severity::LOG_CRIT, - Level::Error => syslog::Severity::LOG_ERR, - Level::Warning => syslog::Severity::LOG_WARNING, - Level::Info => syslog::Severity::LOG_NOTICE, - Level::Debug => syslog::Severity::LOG_INFO, - Level::Trace => syslog::Severity::LOG_DEBUG, - } -} - -/// Drain formatting records and writing them to a syslog ``Logger` -/// -/// Uses mutex to serialize writes. -/// TODO: Add one that does not serialize? -pub struct Streamer3164 { - io: Mutex>, - format: Format3164, -} - -impl Streamer3164 { - /// Create new syslog ``Streamer` using given `format` - pub fn new(logger: Box) -> Self { - Streamer3164 { - io: Mutex::new(logger), - format: Format3164::new(), - } - } -} - -impl Drain for Streamer3164 { - type Err = io::Error; - type Ok = (); - - fn log(&self, info: &Record, logger_values: &OwnedKVList) -> io::Result<()> { - TL_BUF.with(|buf| { - let mut buf = buf.borrow_mut(); - let res = { - || { - try!(self.format.format(&mut *buf, info, logger_values)); - let sever = level_to_severity(info.level()); - { - let io = try!( - self.io - .lock() - .map_err(|_| Error::new(ErrorKind::Other, "locking error")) - ); - - let buf = String::from_utf8_lossy(&buf); - let buf = io.format_3164(sever, &buf).into_bytes(); - - let mut pos = 0; - while pos < buf.len() { - let n = try!(io.send_raw(&buf[pos..])); - if n == 0 { - break; - } - - pos += n; - } - } - - Ok(()) - } - }(); - buf.clear(); - res - }) - } -} - -/// Formatter to format defined in RFC 3164 -pub struct Format3164; - -impl Format3164 { - /// Create new `Format3164` - pub fn new() -> Self { - Format3164 - } - - fn format( - &self, - io: &mut io::Write, - record: &Record, - logger_kv: &OwnedKVList, - ) -> io::Result<()> { - try!(write!(io, "{}", record.msg())); - - let mut ser = KSV::new(io); - { - try!(logger_kv.serialize(record, &mut ser)); - try!(record.kv().serialize(record, &mut ser)); - } - Ok(()) - } -} - -/// Key-Separator-Value serializer -struct KSV { - io: W, -} - -impl KSV { - fn new(io: W) -> Self { - KSV { io: io } - } -} - -impl slog::Serializer for KSV { - fn emit_arguments(&mut self, key: &str, val: &fmt::Arguments) -> slog::Result { - try!(write!(self.io, ", {}: {}", key, val)); - Ok(()) - } -} - -enum SyslogKind { - Unix { - path: PathBuf, - }, - Tcp { - server: SocketAddr, - hostname: String, - }, - Udp { - local: SocketAddr, - host: SocketAddr, - hostname: String, - }, -} - -/// Builder pattern for constructing a syslog -pub struct SyslogBuilder { - facility: Option, - level: syslog::Severity, - logkind: Option, -} -impl Default for SyslogBuilder { - fn default() -> Self { - Self { - facility: None, - level: syslog::Severity::LOG_DEBUG, - logkind: None, - } - } -} -impl SyslogBuilder { - /// Build a default logger - /// - /// By default this will attempt to connect to (in order) - pub fn new() -> SyslogBuilder { - Self::default() - } - - /// Set syslog Facility - pub fn facility(self, facility: syslog::Facility) -> Self { - let mut s = self; - s.facility = Some(facility); - s - } - - /// Filter Syslog by level - pub fn level(self, lvl: slog::Level) -> Self { - let mut s = self; - s.level = level_to_severity(lvl); - s - } - - /// Remote UDP syslogging - pub fn udp>(self, local: SocketAddr, host: SocketAddr, hostname: S) -> Self { - let mut s = self; - let hostname = hostname.as_ref().to_string(); - s.logkind = Some(SyslogKind::Udp { - local, - host, - hostname, - }); - s - } - - /// Remote TCP syslogging - pub fn tcp>(self, server: SocketAddr, hostname: S) -> Self { - let mut s = self; - let hostname = hostname.as_ref().to_string(); - s.logkind = Some(SyslogKind::Tcp { server, hostname }); - s - } - - /// Local syslogging over a unix socket - pub fn unix>(self, path: P) -> Self { - let mut s = self; - let path = path.as_ref().to_path_buf(); - s.logkind = Some(SyslogKind::Unix { path }); - s - } - - /// Start running - pub fn start(self) -> io::Result { - let facility = match self.facility { - Option::Some(x) => x, - Option::None => { - return Err(Error::new( - ErrorKind::Other, - "facility must be provided to the builder", - )); - } - }; - let logkind = match self.logkind { - Option::Some(l) => l, - Option::None => { - return Err(Error::new( - ErrorKind::Other, - "no logger kind provided, library does not know what do initialize", - )); - } - }; - let log = match logkind { - SyslogKind::Unix { path } => syslog::unix_custom(facility, path)?, - SyslogKind::Udp { - local, - host, - hostname, - } => syslog::udp(local, host, hostname, facility)?, - SyslogKind::Tcp { server, hostname } => syslog::tcp(server, hostname, facility)?, - }; - Ok(Streamer3164::new(log)) - } -} - -/// `Streamer` to Unix syslog using RFC 3164 format -pub fn unix_3164(facility: syslog::Facility) -> io::Result { - syslog::unix(facility).map(Streamer3164::new) -} diff --git a/src/builder.rs b/src/builder.rs new file mode 100644 index 0000000..a226997 --- /dev/null +++ b/src/builder.rs @@ -0,0 +1,269 @@ +use Facility; +use format::{DefaultMsgFormat, MsgFormat}; +use libc; +use SyslogDrain; +use std::borrow::Cow; +use std::ffi::{CStr, CString}; + +/// Builds a [`SyslogDrain`]. +/// +/// All settings have sensible defaults. Simply calling +/// `SyslogBuilder::new().build()` (or `SyslogDrain::new()`, which is +/// equivalent) will yield a functional, reasonable `Drain` in most +/// situations. However, most applications will want to set the `facility`. +/// +/// [`SyslogDrain`]: struct.SyslogDrain.html +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct SyslogBuilder { + pub(crate) facility: Facility, + pub(crate) ident: Option>, + pub(crate) option: libc::c_int, + pub(crate) format: F, +} + +impl Default for SyslogBuilder { + fn default() -> Self { + SyslogBuilder { + facility: Facility::default(), + ident: None, + option: 0, + format: DefaultMsgFormat, + } + } +} + +impl SyslogBuilder { + /// Makes a new `SyslogBuilder` instance. + pub fn new() -> Self { + SyslogBuilder::default() + } +} + +impl SyslogBuilder { + /// Sets the syslog facility to send logs to. + /// + /// By default, this is [`Facility::User`]. + /// + /// [`Facility::User`]: enum.Facility.html#variant.User + pub fn facility(mut self, facility: Facility) -> Self { + self.facility = facility; + self + } + + /// Sets the name of this program, for inclusion with log messages. + /// (POSIX calls this the “tag”.) + /// + /// The supplied string must not contain any zero (ASCII NUL) bytes. + /// + /// # Default value + /// + /// If a name is not given, the default behavior depends on the libc + /// implementation in use. + /// + /// BSD, GNU, and Apple libc use the actual process name. µClibc uses the + /// constant string `syslog`. Fuchsia libc and musl libc use no name at + /// all. + /// + /// # When to use + /// + /// This method converts the given string to a C-compatible string at run + /// time. It should only be used if the process name is obtained + /// dynamically, such as from a configuration file. + /// + /// If the process name is constant, use the `ident` method instead. + /// + /// # Panics + /// + /// This method panics if the supplied string contains any null bytes. + /// + /// # Example + /// + /// ``` + /// use slog_syslog::SyslogBuilder; + /// + /// # let some_string = "hello".to_string(); + /// let my_ident: String = some_string; + /// + /// let drain = SyslogBuilder::new() + /// .ident_str(my_ident) + /// .build(); + /// ``` + /// + /// # Data use and lifetime + /// + /// This method takes an ordinary Rust string, copies it into a + /// [`CString`] (which appends a null byte on the end), and passes that to + /// the `ident` method. + /// + /// [`CString`]: https://doc.rust-lang.org/std/ffi/struct.CString.html + pub fn ident_str>(self, ident: S) -> Self { + let cs = CString::new(ident.as_ref()) + .expect("`SyslogBuilder::ident` called with string that contains null bytes"); + + self.ident(Cow::Owned(cs)) + } + + /// Sets the name of this program, for inclusion with log messages. + /// (POSIX calls this the “tag”.) + /// + /// # Default value + /// + /// If a name is not given, the default behavior depends on the libc + /// implementation in use. + /// + /// BSD, GNU, and Apple libc use the actual process name. µClibc uses the + /// constant string `syslog`. Fuchsia libc and musl libc use no name at + /// all. + /// + /// # When to use + /// + /// This method should be used if you already have a C-compatible string to + /// use for the process name, or if the process name is constant (as + /// opposed to taken from a configuration file or command line parameter). + /// + /// # Data use and lifetime + /// + /// This method takes a C-compatible string, either owned or with the + /// `'static` lifetime. This ensures that the string remains available for + /// the entire time that the system libc might need it (until `closelog` is + /// called, which happens when the `SyslogDrain` is dropped). + /// + /// # Example + /// + /// ``` + /// use slog_syslog::SyslogBuilder; + /// use std::ffi::CStr; + /// + /// let drain = SyslogBuilder::new() + /// .ident(CStr::from_bytes_with_nul("example-app\0".as_bytes()).unwrap()) + /// .build(); + /// ``` + pub fn ident>>(mut self, ident: S) -> Self { + self.ident = Some(ident.into()); + self + } + + // The `log_*` flag methods are all `#[inline]` because, in theory, the + // optimizer could collapse several flag method calls into a single store + // operation, which would be much faster…but it can only do that if the + // calls are all inlined. + + /// Include the process ID in log messages. + #[inline] + pub fn log_pid(mut self) -> Self { + self.option |= libc::LOG_PID; + self + } + + /// Immediately open a connection to the syslog server, instead of waiting + /// until the first log message is sent. + /// + /// `log_ndelay` and `log_odelay` are mutually exclusive, and one of them + /// is the default. Exactly which one is the default depends on the + /// platform, but on most platforms, `log_odelay` is the default. + /// + /// On OpenBSD 5.6 and newer, this setting has no effect, because that + /// platform uses a dedicated system call instead of a socket for + /// submitting syslog messages. + #[inline] + pub fn log_ndelay(mut self) -> Self { + self.option = (self.option & !libc::LOG_ODELAY) | libc::LOG_NDELAY; + self + } + + /// *Don't* immediately open a connection to the syslog server. Wait until + /// the first log message is sent before connecting. + /// + /// `log_ndelay` and `log_odelay` are mutually exclusive, and one of them + /// is the default. Exactly which one is the default depends on the + /// platform, but on most platforms, `log_odelay` is the default. + /// + /// On OpenBSD 5.6 and newer, this setting has no effect, because that + /// platform uses a dedicated system call instead of a socket for + /// submitting syslog messages. + #[inline] + pub fn log_odelay(mut self) -> Self { + self.option = (self.option & !libc::LOG_NDELAY) | libc::LOG_ODELAY; + self + } + + /// If a child process is created to send a log message, don't wait for + /// that child process to exit. + /// + /// This option is highly unlikely to have any effect on any modern system. + /// On a modern system, spawning a child process for every single log + /// message would be extremely slow. This option only ever existed as a + /// [workaround for limitations of the 2.11BSD kernel][2.11BSD wait call], + /// and was already [deprecated as of 4.4BSD][4.4BSD deprecation notice]. + /// It is included here only for completeness because, unfortunately, + /// [POSIX defines it]. + /// + /// [2.11BSD wait call]: https://www.retro11.de/ouxr/211bsd/usr/src/lib/libc/gen/syslog.c.html#n:176 + /// [4.4BSD deprecation notice]: https://github.com/sergev/4.4BSD-Lite2/blob/50587b00e922225c62f1706266587f435898126d/usr/src/sys/sys/syslog.h#L164 + /// [POSIX defines it]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html + #[inline] + pub fn log_nowait(mut self) -> Self { + self.option |= libc::LOG_NOWAIT; + self + } + + /// Also emit log messages on `stderr` (**see warning**). + /// + /// # Warning + /// + /// The libc `syslog` function is not subject to the global mutex that + /// Rust uses to synchronize access to `stderr`. As a result, if one thread + /// writes to `stderr` at the same time as another thread emits a log + /// message with this option, the log message may appear in the middle of + /// the other thread's output. + /// + /// Note that this problem is not specific to Rust or this crate. Any + /// program in any language that writes to `stderr` in one thread and logs + /// to `syslog` with `LOG_PERROR` in another thread at the same time will + /// have the same problem. + /// + /// The exception is the `syslog` implementation in GNU libc, which + /// implements this option by writing to `stderr` through the C `stdio` + /// API (as opposed to the `write` system call), which has its own mutex. + /// As long as all threads write to `stderr` using the C `stdio` API, log + /// messages on this platform will never appear in the middle of other + /// `stderr` output. However, Rust does not use the C `stdio` API for + /// writing to `stderr`, so even on GNU libc, using this option may result + /// in garbled output. + #[inline] + pub fn log_perror(mut self) -> Self { + self.option |= libc::LOG_PERROR; + self + } + + /// Set a format for log messages and structured data. + /// + /// The default is [`DefaultMsgFormat`]. + /// + /// # Example + /// + /// ``` + /// use slog_syslog::SyslogBuilder; + /// use slog_syslog::format::BasicMsgFormat; + /// + /// let logger = SyslogBuilder::new() + /// .format(BasicMsgFormat) + /// .build(); + /// ``` + /// + /// [`DefaultMsgFormat`]: format/struct.DefaultMsgFormat.html + pub fn format(self, format: F2) -> SyslogBuilder { + SyslogBuilder { + facility: self.facility, + ident: self.ident, + option: self.option, + format, + } + } + + /// Builds a `SyslogDrain` from the settings provided. + pub fn build(self) -> SyslogDrain { + SyslogDrain::from_builder(self) + } +} diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..accaadf --- /dev/null +++ b/src/config.rs @@ -0,0 +1,231 @@ +//! Adapters for configuring a [`SyslogDrain`] from a configuration file using +//! [serde]. Requires Cargo feature `serde`. +//! +//! [serde]: https://serde.rs/ +//! [`SyslogDrain`]: ../struct.SyslogDrain.html + +use Facility; +use format::{BasicMsgFormat, DefaultMsgFormat, MsgFormat}; +use slog::{self, OwnedKVList, Record}; +use std::borrow::Cow; +use std::ffi::CStr; +use std::fmt; +use SyslogBuilder; +use SyslogDrain; +#[cfg(test)] use toml; + +/// Deserializable configuration for a [`SyslogDrain`]. +/// +/// Call the [`build`] method to create a [`SyslogDrain`] from a +/// `SyslogConfig`. +/// +/// [`build`]: #method.build +/// [`SyslogDrain`]: ../struct.SyslogDrain.html +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(default)] +pub struct SyslogConfig { + /// How to format syslog messages with structured data. + /// + /// Possible values are `default` and `basic`. + /// + /// See [`MsgFormat`] for more information. + /// + /// [`MsgFormat`]: ../format/trait.MsgFormat.html + pub format: MsgFormatConfig, + + /// The syslog facility to send logs to. + pub facility: Facility, + + /// The name of this program, for inclusion with log messages. (POSIX calls + /// this the “tag”.) + /// + /// The string must not contain any zero (ASCII NUL) bytes. + /// + /// # Default value + /// + /// If a name is not given, the default behavior depends on the libc + /// implementation in use. + /// + /// BSD, GNU, and Apple libc use the actual process name. µClibc uses the + /// constant string `syslog`. Fuchsia libc and musl libc use no name at + /// all. + pub ident: Option>, + + /// Include the process ID in log messages. + pub log_pid: bool, + + /// Whether to immediately open a connection to the syslog server. + /// + /// If true, a connection will be immediately opened. If false, the + /// connection will only be opened when the first log message is submitted. + /// + /// The default is platform-defined, but on most platforms, the default is + /// `true`. + /// + /// On OpenBSD 5.6 and newer, this setting has no effect, because that + /// platform uses a dedicated system call instead of a socket for + /// submitting syslog messages. + pub log_delay: Option, + + /// Also emit log messages on `stderr` (**see warning**). + /// + /// # Warning + /// + /// The libc `syslog` function is not subject to the global mutex that + /// Rust uses to synchronize access to `stderr`. As a result, if one thread + /// writes to `stderr` at the same time as another thread emits a log + /// message with this option, the log message may appear in the middle of + /// the other thread's output. + /// + /// Note that this problem is not specific to Rust or this crate. Any + /// program in any language that writes to `stderr` in one thread and logs + /// to `syslog` with `LOG_PERROR` in another thread at the same time will + /// have the same problem. + /// + /// The exception is the `syslog` implementation in GNU libc, which + /// implements this option by writing to `stderr` through the C `stdio` + /// API (as opposed to the `write` system call), which has its own mutex. + /// As long as all threads write to `stderr` using the C `stdio` API, log + /// messages on this platform will never appear in the middle of other + /// `stderr` output. However, Rust does not use the C `stdio` API for + /// writing to `stderr`, so even on GNU libc, using this option may result + /// in garbled output. + pub log_perror: bool, + + #[serde(skip)] + __non_exhaustive: (), +} + +impl SyslogConfig { + /// Creates a new `SyslogConfig` with default settings. + pub fn new() -> Self { + Default::default() + } + + /// Creates a new `SyslogBuilder` from the settings. + pub fn into_builder(self) -> SyslogBuilder { + let b = SyslogBuilder::new() + .facility(self.facility) + .format(self.format.into()); + + let b = match self.ident { + Some(ident) => b.ident(ident), + None => b, + }; + + let b = match self.log_pid { + true => b.log_pid(), + false => b, + }; + + let b = match self.log_delay { + Some(true) => b.log_odelay(), + Some(false) => b.log_ndelay(), + None => b, + }; + + let b = match self.log_perror { + true => b.log_perror(), + false => b, + }; + + b + } + + /// Creates a new `SyslogDrain` from the settings. + pub fn build(self) -> SyslogDrain { + self.into_builder().build() + } +} + +impl Default for SyslogConfig { + fn default() -> Self { + SyslogConfig { + format: MsgFormatConfig::default(), + facility: Facility::default(), + ident: None, + log_pid: false, + log_delay: None, + log_perror: false, + __non_exhaustive: (), + } + } +} + +/// Enumeration of built-in `MsgFormat`s. +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +#[serde(rename_all = "snake_case")] +pub enum MsgFormatConfig { + /// [`DefaultMsgFormat`](struct.DefaultMsgFormat.html). + Default, + + /// [`BasicMsgFormat`](struct.BasicMsgFormat.html). + Basic, + + #[doc(hidden)] + __NonExhaustive, +} + +impl Default for MsgFormatConfig { + fn default() -> Self { + MsgFormatConfig::Default + } +} + +/// Implements [`MsgFormat`] based on the settings in a [`MsgFormatConfig`]. +/// +/// This is the type of [`MsgFormat`] used by [`SyslogDrain`]s constructed from +/// a [`SyslogConfig`]. +/// +/// [`MsgFormat`]: ../format/trait.MsgFormat.html +/// [`MsgFormatConfig`]: enum.MsgFormatConfig.html +/// [`SyslogConfig`]: struct.SyslogConfig.html +/// [`SyslogDrain`]: ../struct.SyslogDrain.html +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct ConfiguredMsgFormat { + config: MsgFormatConfig, +} + +impl MsgFormat for ConfiguredMsgFormat { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + match self.config { + MsgFormatConfig::Basic => BasicMsgFormat.fmt(f, record, values), + MsgFormatConfig::Default => DefaultMsgFormat.fmt(f, record, values), + MsgFormatConfig::__NonExhaustive => panic!("MsgFormatConfig::__NonExhaustive used") + } + } +} + +impl From for ConfiguredMsgFormat { + fn from(config: MsgFormatConfig) -> Self { + ConfiguredMsgFormat { + config + } + } +} + +#[test] +fn test_config() { + const TOML_CONFIG: &'static str = r#" +format = "basic" +ident = "foo" +facility = "daemon" +log_pid = true +log_perror = true +"#; + + let config: SyslogConfig = toml::de::from_str(TOML_CONFIG).expect("deserialization failed"); + + let builder = config.into_builder(); + + assert_eq!( + builder, + SyslogBuilder::new() + .format(ConfiguredMsgFormat::from(MsgFormatConfig::Basic)) + .ident_str("foo") + .facility(Facility::Daemon) + .log_pid() + .log_perror() + ); +} diff --git a/src/drain.rs b/src/drain.rs new file mode 100644 index 0000000..75df74f --- /dev/null +++ b/src/drain.rs @@ -0,0 +1,314 @@ +use builder::SyslogBuilder; +use format::{DefaultMsgFormat, format, MsgFormat}; +use libc::{self, c_char, c_int}; +use slog::{self, Drain, Level, Record, OwnedKVList}; +use std::borrow::Cow; +use std::cell::RefCell; +use std::ffi::CStr; +use std::io::{self, Write}; +use std::ptr; +use std::sync::{Mutex, MutexGuard}; + +#[cfg(not(test))] +use libc::{closelog, openlog, syslog}; +#[cfg(test)] +use mock::{self, closelog, openlog, syslog}; + +thread_local! { + static TL_BUF: RefCell> = RefCell::new(Vec::with_capacity(128)) +} + +lazy_static! { + /// Keeps track of which `ident` string was most recently passed to `openlog`. + /// + /// The mutex is to be locked while calling `openlog` or `closelog`. It + /// contains a possibly-null pointer to the `ident` string most recently passed + /// to `openlog`, if that pointer came from `CStr::as_ptr`. + /// + /// The pointer is stored as a `usize` because pointers are `!Send`. It is only + /// used for comparison, never dereferenced. + /// + /// # Purpose and rationale + /// + /// The POSIX `openlog` function accepts a pointer to a C string. Though POSIX + /// does not specify the expected lifetime of the string, all known + /// implementations either + /// + /// 1. keep the pointer in a global variable, or + /// 2. copy the string into an internal buffer, which is kept in a global + /// variable. + /// + /// When running with an implementation in the second category, the string may + /// be safely freed right away. When running with an implementation in the + /// first category, however, the string must not be freed until either + /// `closelog` is called or `openlog` is called with a *different, non-null* + /// `ident`. + /// + /// This mutex keeps track of which `ident` was most recently passed, making it + /// possible to decide whether `closelog` needs to be called before a given + /// `ident` string is dropped. + /// + /// (Note: In the original 4.4BSD source code, the pointer is kept in a global + /// variable, but `closelog` does *not* clear the pointer. In this case, it is + /// only safe to free the string after `openlog` has been called with a + /// different, non-null `ident`. Fortunately, all present-day implementations + /// of `closelog` either clear the pointer or don't retain it at all.) + static ref LAST_UNIQUE_IDENT: Mutex = Mutex::new(ptr::null::() as usize); +} + +/// [`Drain`] implementation that sends log messages to syslog. +/// +/// [`Drain`]: https://docs.rs/slog/2/slog/trait.Drain.html +#[derive(Debug)] +pub struct SyslogDrain { + /// The `ident` string, if it is owned by this `SyslogDrain`. + /// + /// This is kept so that the string can be freed (and `closelog` called, if + /// necessary) when this `SyslogDrain` is dropped. + unique_ident: Option>, + + /// The format for log messages. + format: F, +} + +impl SyslogDrain { + /// Creates a new `SyslogDrain` with all default settings. + /// + /// Equivalent to `SyslogBuilder::new().build()`. + pub fn new() -> Self { + SyslogBuilder::new().build() + } +} + +impl SyslogDrain { + /// Creates a new `SyslogBuilder`. + /// + /// Equivalent to `SyslogBuilder::new()`. + #[inline] + pub fn builder() -> SyslogBuilder { + SyslogBuilder::new() + } + + pub(crate) fn from_builder(builder: SyslogBuilder) -> Self { + // `ident` is the pointer that will be passed to `openlog`, maybe null. + // + // `unique_ident` is the same pointer, wrapped in `Some` and `NonNull`, + // but only if the `ident` string provided by the application is owned. + // Otherwise it's `None`, indicating that `ident` either is null or + // points to a `&'static` string. + let (ident, unique_ident): (*const c_char, Option>) = match builder.ident.clone() { + Some(Cow::Owned(ident_s)) => { + let unique_ident = ident_s.into_boxed_c_str(); + + // Calling `NonNull:new_unchecked` is correct because + // `CString::into_raw` never returns a null pointer. + (unique_ident.as_ptr(), Some(unique_ident)) + } + Some(Cow::Borrowed(ident_s)) => (ident_s.as_ptr(), None), + None => (ptr::null(), None), + }; + + { + // `openlog` and `closelog` are only called while holding the mutex + // around `last_unique_ident`. + let mut last_unique_ident: MutexGuard = LAST_UNIQUE_IDENT.lock().unwrap(); + + // Here, we call `openlog`. This has to happen *before* freeing the + // previous `ident` string, if applicable. + unsafe { openlog(ident, builder.option, builder.facility.into()); } + + // If `openlog` is called with a null `ident` pointer, then the + // `ident` string passed to it previously will remain in use. But + // if the `ident` pointer is not null, then `last_unique_ident` + // needs updating. + if !ident.is_null() { + *last_unique_ident = match &unique_ident { + // If the `ident` string is owned, store the pointer to it. + Some(s) => s.as_ptr(), + + // If the `ident` string is not owned, set the stored + // pointer to null. + None => ptr::null::(), + } as usize; + } + } + + SyslogDrain { + unique_ident, + format: builder.format, + } + } +} + +impl Drop for SyslogDrain { + fn drop(&mut self) { + // Check if this `SyslogDrain` was created with an owned `ident` + // string. + if let Some(my_ident) = self.unique_ident.take() { + // If so, then we need to check if that string is the one that + // was most recently passed to `openlog`. + let mut last_unique_ident: MutexGuard = match LAST_UNIQUE_IDENT.lock() { + Ok(locked) => locked, + + // If the mutex was poisoned, then we'll just let the + // string leak. + // + // There's no point in panicking here, and if there was a + // panic after `openlog` but before the pointer in the + // mutex was updated, then trying to free the pointed-to + // string may result in undefined behavior from a double + // free. + // + // Thankfully, Rust's standard mutex implementation + // supports poisoning. Some alternative mutex + // implementations, such as in the `parking_lot` crate, + // don't support poisoning and would expose us to the + // aforementioned undefined behavior. + // + // It would be nice if we could un-poison a poisoned mutex, + // though. We have a perfectly good recovery strategy for + // that situation (resetting its pointer to null), but no way + // to use it. + Err(_) => { + Box::leak(my_ident); + return; + } + }; + + if my_ident.as_ptr() as usize == *last_unique_ident { + // Yes, the most recently used string was ours. We need to + // call `closelog` before our string is dropped. + // + // Note that this isn't completely free of races. It's still + // possible for some other code to call `openlog` independently + // of this module, after our `openlog` call. In that case, this + // `closelog` call will incorrectly close *that* logging handle + // instead of the one belonging to this `SyslogDrain`. + // + // Behavior in that case is still well-defined. Subsequent + // calls to `syslog` will implicitly reopen the logging handle + // anyway. The only problem is that the `openlog` options + // (facility, program name, etc) will all be reset. For this + // reason, it is a bad idea for a library to call `openlog` (or + // construct a `SyslogDrain`!) except when instructed to do so + // by the main program. + unsafe { closelog(); } + + // Also, be sure to reset the pointer stored in the mutex. + // Although it is never dereferenced, letting it dangle may + // cause the above `if` to test true when it shouldn't, which + // would result in `closelog` being called when it shouldn't. + *last_unique_ident = ptr::null::() as usize; + } + + // When testing, before dropping the owned string, copy it into + // a mock event. We'll still drop it, though, in order to test for + // double-free bugs. + #[cfg(test)] + mock::push_event(mock::Event::DropOwnedIdent( + String::from(my_ident.to_string_lossy()) + )); + + // Now that `closelog` has been called, it's safe for our string to + // be dropped, which will happen here. + } + } +} + +impl Drain for SyslogDrain { + type Ok = (); + type Err = slog::Never; + + fn log(&self, record: &Record, values: &OwnedKVList) -> Result { + TL_BUF.with(|tl_buf_ref| { + let mut tl_buf_mut = tl_buf_ref.borrow_mut(); + let mut tl_buf = &mut *tl_buf_mut; + + // Figure out the priority. + let priority: c_int = match record.level() { + Level::Critical => libc::LOG_CRIT, + Level::Error => libc::LOG_ERR, + Level::Warning => libc::LOG_WARNING, + Level::Debug | Level::Trace => libc::LOG_DEBUG, + + // `slog::Level` isn't non-exhaustive, so adding any more levels + // would be a breaking change. That is highly unlikely to ever + // happen. Still, we'll handle the possibility here, just in case. + _ => libc::LOG_INFO + }; + + // Format the message. + let fmt_err = format(&self.format, &mut tl_buf, record, values).err(); + + // If formatting fails, use an effectively null format (which shouldn't + // ever fail), and separately log the error. + if fmt_err.is_some() { + tl_buf.clear(); + assert_format_success(write!(tl_buf, "{}", record.msg())); + } + + { + // Convert both strings to C strings. + let msg = make_cstr_lossy(tl_buf); + + // All set. Submit the log message. + unsafe { + syslog( + priority, + CStr::from_bytes_with_nul_unchecked(b"%s\0").as_ptr(), + msg.as_ptr() + ); + } + } + + // Clean up. + tl_buf.clear(); + + // If there was a formatting error, log that too. + if let Some(fmt_err) = fmt_err { + assert_format_success(write!(tl_buf, "{}", fmt_err)); + + { + let msg = make_cstr_lossy(tl_buf); + + unsafe { + syslog( + libc::LOG_ERR, + CStr::from_bytes_with_nul_unchecked(b"Error fully formatting the previous log message: %s\0").as_ptr(), + msg.as_ptr() + ); + } + } + + // Clean up again. + tl_buf.clear(); + } + + // Done. + Ok(()) + }) + } +} + +/// Creates a `&CStr` from the given `Vec`, removing middle null bytes and +/// adding a null terminator as needed. +fn make_cstr_lossy(s: &mut Vec) -> &CStr { + // Strip any null bytes from the string. + s.retain(|b| *b != 0); + + // Add a null terminator. + s.push(0); + + // This is sound because we just stripped all the null bytes from the + // input (except the one at the end). + unsafe { CStr::from_bytes_with_nul_unchecked(&*s) } +} + +/// Panics on I/O error, but only in debug builds. +/// +/// Used for `io::Write`s into a `Vec`, which should never fail. +#[inline] +fn assert_format_success(_result: io::Result<()>) { + #[cfg(debug)] + _result.expect("unexpected formatting error"); +} diff --git a/src/facility.rs b/src/facility.rs new file mode 100644 index 0000000..83dad9d --- /dev/null +++ b/src/facility.rs @@ -0,0 +1,532 @@ +use libc::{self, c_int}; +use std::error::Error; +use std::fmt::{self, Display}; +use std::str::FromStr; + +/// A syslog facility. Conversions are provided to and from `c_int`. +/// +/// Available facilities depend on the target platform. All variants of this +/// `enum` are available on all platforms, and variants not present on the +/// target platform will be mapped to a reasonable alternative. +/// +/// The default facility is [`User`]. +/// +/// [`User`]: #variant.User +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +pub enum Facility { + /// Authentication, authorization, and other security-related matters. + /// + /// Available on: all platforms + Auth, + + /// Log messages containing sensitive information. + /// + /// Available on: Linux, Emscripten, macOS, iOS, FreeBSD, DragonFly BSD, + /// OpenBSD, NetBSD + /// + /// On other platforms: becomes `Auth` + AuthPriv, + + /// Periodic task scheduling daemons like `cron`. + /// + /// Available on: Linux, Emscripten, macOS, iOS, FreeBSD, DragonFly BSD, + /// OpenBSD, NetBSD, Solaris, illumos + /// + /// On other platforms: becomes `Daemon` + Cron, + + /// Daemons that don't fall into a more specific category. + /// + /// Available on: all platforms + Daemon, + + /// FTP server. + /// + /// Available on: Linux, Emscripten, macOS, iOS, FreeBSD, DragonFly BSD, + /// OpenBSD, NetBSD + /// + /// On other platforms: becomes `Daemon` + Ftp, + + /// Operating system kernel. + /// + /// Note: Programs other than the kernel are typically not allowed to use + /// this facility. + /// + /// Available on: all platforms + Kern, + + /// macOS installer. + /// + /// Available on: macOS, iOS + /// + /// On other platforms: becomes `User` + Install, + + /// `launchd`, the macOS process supervisor. + /// + /// Available on: macOS, iOS + /// + /// On other platforms: becomes `Daemon` + Launchd, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local0, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local1, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local2, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local3, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local4, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local5, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local6, + + /// Reserved for local use. + /// + /// Available on: all platforms + Local7, + + /// Print server. + /// + /// Available on: all platforms + Lpr, + + /// Mail transport and delivery agents. + /// + /// Available on: all platforms + Mail, + + /// Network Time Protocol daemon. + /// + /// Available on: FreeBSD, DragonFly BSD + /// + /// On other platforms: becomes `Daemon` + Ntp, + + /// NeXT/early macOS `NetInfo` system. + /// + /// Note: Obsolete on modern macOS. + /// + /// Available on: macOS, iOS + /// + /// On other platforms: becomes `Daemon` + NetInfo, + + /// Usenet news system. + /// + /// Available on: all platforms + News, + + /// macOS Remote Access Service. + /// + /// Available on: macOS, iOS + /// + /// On other platforms: becomes `User` + Ras, + + /// macOS remote authentication and authorization. + /// + /// Available on: macOS, iOS + /// + /// On other platforms: becomes `Daemon` + RemoteAuth, + + /// Security subsystems. + /// + /// Available on: FreeBSD, DragonFly BSD + /// + /// On other platforms: becomes `Auth` + Security, + + /// Messages generated internally by the syslog daemon. + /// + /// Available on: all platforms + Syslog, + + /// General user processes. + /// + /// Note: This is the default facility (that is, the value returned by `Facility::default()`). + /// + /// Available on: all platforms + User, + + /// Unix-to-Unix Copy system. + /// + /// Available on: all platforms + Uucp, + + #[doc(hidden)] + __NonExhaustive +} + +impl Facility { + /// Gets the name of this `Facility`, in lowercase. + /// + /// The `FromStr` implementation accepts the same names, but it is + /// case-insensitive. + pub fn name(&self) -> &'static str { + #[allow(deprecated)] + match *self { + Facility::Auth => "auth", + Facility::AuthPriv => "authpriv", + Facility::Cron => "cron", + Facility::Daemon => "daemon", + Facility::Ftp => "ftp", + Facility::Kern => "kern", + Facility::Install => "install", + Facility::Launchd => "launchd", + Facility::Local0 => "local0", + Facility::Local1 => "local1", + Facility::Local2 => "local2", + Facility::Local3 => "local3", + Facility::Local4 => "local4", + Facility::Local5 => "local5", + Facility::Local6 => "local6", + Facility::Local7 => "local7", + Facility::Lpr => "lpr", + Facility::Mail => "mail", + Facility::Ntp => "ntp", + Facility::NetInfo => "netinfo", + Facility::News => "news", + Facility::Ras => "ras", + Facility::RemoteAuth => "remoteauth", + Facility::Security => "security", + Facility::Syslog => "syslog", + Facility::User => "user", + Facility::Uucp => "uucp", + Facility::__NonExhaustive => panic!("Facility::__NonExhaustive used") + } + } + + /// Converts a `libc::LOG_*` numeric constant to a `Facility` value. + /// + /// Returns `Some` if the value is a valid facility identifier, or `None` + /// if not. + pub fn from_int(value: c_int) -> Option { + match value { + libc::LOG_AUTH => Some(Facility::Auth), + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + ))] + libc::LOG_AUTHPRIV => Some(Facility::AuthPriv), + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_env = "uclibc" + ))] + libc::LOG_CRON => Some(Facility::Cron), + libc::LOG_DAEMON => Some(Facility::Daemon), + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + ))] + libc::LOG_FTP => Some(Facility::Ftp), + libc::LOG_KERN => Some(Facility::Kern), + #[cfg(any(target_os = "macos", target_os = "ios"))] + libc::LOG_INSTALL => Some(Facility::Install), + #[cfg(any(target_os = "macos", target_os = "ios"))] + libc::LOG_LAUNCHD => Some(Facility::Launchd), + libc::LOG_LOCAL0 => Some(Facility::Local0), + libc::LOG_LOCAL1 => Some(Facility::Local1), + libc::LOG_LOCAL2 => Some(Facility::Local2), + libc::LOG_LOCAL3 => Some(Facility::Local3), + libc::LOG_LOCAL4 => Some(Facility::Local4), + libc::LOG_LOCAL5 => Some(Facility::Local5), + libc::LOG_LOCAL6 => Some(Facility::Local6), + libc::LOG_LOCAL7 => Some(Facility::Local7), + libc::LOG_LPR => Some(Facility::Lpr), + libc::LOG_MAIL => Some(Facility::Mail), + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + libc::LOG_NTP => Some(Facility::Ntp), + #[cfg(any(target_os = "macos", target_os = "ios"))] + libc::LOG_NETINFO => Some(Facility::NetInfo), + libc::LOG_NEWS => Some(Facility::News), + #[cfg(any(target_os = "macos", target_os = "ios"))] + libc::LOG_RAS => Some(Facility::Ras), + #[cfg(any(target_os = "macos", target_os = "ios"))] + libc::LOG_REMOTEAUTH => Some(Facility::RemoteAuth), + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + libc::LOG_SECURITY => Some(Facility::Security), + libc::LOG_SYSLOG => Some(Facility::Syslog), + libc::LOG_USER => Some(Facility::User), + libc::LOG_UUCP => Some(Facility::Uucp), + _ => None + } + } +} + +impl Default for Facility { + fn default() -> Self { + Facility::User + } +} + +impl Display for Facility { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl From for c_int { + fn from(facility: Facility) -> Self { + #[allow(deprecated)] + match facility { + Facility::Auth => libc::LOG_AUTH, + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + ))] + Facility::AuthPriv => libc::LOG_AUTHPRIV, + #[cfg(not(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + )))] + Facility::AuthPriv => libc::LOG_AUTH, + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_env = "uclibc" + ))] + Facility::Cron => libc::LOG_CRON, + #[cfg(not(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_os = "solaris", + target_os = "illumos", + target_env = "uclibc" + )))] + Facility::Cron => libc::LOG_DAEMON, + Facility::Daemon => libc::LOG_DAEMON, + #[cfg(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + ))] + Facility::Ftp => libc::LOG_FTP, + #[cfg(not(any( + target_os = "linux", + target_os = "android", + target_os = "emscripten", + target_os = "macos", + target_os = "ios", + target_os = "freebsd", + target_os = "dragonfly", + target_os = "openbsd", + target_os = "netbsd", + target_env = "uclibc" + )))] + Facility::Ftp => libc::LOG_DAEMON, + Facility::Kern => libc::LOG_KERN, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Facility::Install => libc::LOG_INSTALL, + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Facility::Install => libc::LOG_USER, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Facility::Launchd => libc::LOG_LAUNCHD, + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Facility::Launchd => libc::LOG_DAEMON, + Facility::Local0 => libc::LOG_LOCAL0, + Facility::Local1 => libc::LOG_LOCAL1, + Facility::Local2 => libc::LOG_LOCAL2, + Facility::Local3 => libc::LOG_LOCAL3, + Facility::Local4 => libc::LOG_LOCAL4, + Facility::Local5 => libc::LOG_LOCAL5, + Facility::Local6 => libc::LOG_LOCAL6, + Facility::Local7 => libc::LOG_LOCAL7, + Facility::Lpr => libc::LOG_LPR, + Facility::Mail => libc::LOG_MAIL, + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + Facility::Ntp => libc::LOG_NTP, + #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly")))] + Facility::Ntp => libc::LOG_DAEMON, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Facility::NetInfo => libc::LOG_NETINFO, + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Facility::NetInfo => libc::LOG_DAEMON, + Facility::News => libc::LOG_NEWS, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Facility::Ras => libc::LOG_RAS, + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Facility::Ras => libc::LOG_USER, + #[cfg(any(target_os = "macos", target_os = "ios"))] + Facility::RemoteAuth => libc::LOG_REMOTEAUTH, + #[cfg(not(any(target_os = "macos", target_os = "ios")))] + Facility::RemoteAuth => libc::LOG_DAEMON, + #[cfg(any(target_os = "freebsd", target_os = "dragonfly"))] + Facility::Security => libc::LOG_SECURITY, + #[cfg(not(any(target_os = "freebsd", target_os = "dragonfly")))] + Facility::Security => libc::LOG_AUTH, + Facility::Syslog => libc::LOG_SYSLOG, + Facility::User => libc::LOG_USER, + Facility::Uucp => libc::LOG_UUCP, + Facility::__NonExhaustive => panic!("Facility::__NonExhaustive used") + } + } +} + +impl FromStr for Facility { + type Err = UnknownFacilityError; + + fn from_str(s: &str) -> Result { + let s = s.to_ascii_lowercase(); + + match &*s { + "auth" => Ok(Facility::Auth), + "authpriv" => Ok(Facility::AuthPriv), + "cron" => Ok(Facility::Cron), + "daemon" => Ok(Facility::Daemon), + "ftp" => Ok(Facility::Ftp), + "kern" => Ok(Facility::Kern), + "install" => Ok(Facility::Install), + "launchd" => Ok(Facility::Launchd), + "local0" => Ok(Facility::Local0), + "local1" => Ok(Facility::Local1), + "local2" => Ok(Facility::Local2), + "local3" => Ok(Facility::Local3), + "local4" => Ok(Facility::Local4), + "local5" => Ok(Facility::Local5), + "local6" => Ok(Facility::Local6), + "local7" => Ok(Facility::Local7), + "lpr" => Ok(Facility::Lpr), + "mail" => Ok(Facility::Mail), + "ntp" => Ok(Facility::Ntp), + "netinfo" => Ok(Facility::NetInfo), + "news" => Ok(Facility::News), + "ras" => Ok(Facility::Ras), + "remoteauth" => Ok(Facility::RemoteAuth), + "security" => Ok(Facility::Security), + "syslog" => Ok(Facility::Syslog), + "user" => Ok(Facility::User), + "uucp" => Ok(Facility::Uucp), + _ => Err(UnknownFacilityError { + name: s, + }) + } + } +} + +/// Indicates that `::from_str` was called with an unknown +/// facility name. +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct UnknownFacilityError { + name: String, +} + +impl UnknownFacilityError { + /// The unrecognized facility name. + pub fn name(&self) -> &str { + &*self.name + } +} + +impl Display for UnknownFacilityError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unrecognized syslog facility name `{}`", self.name) + } +} + +impl Error for UnknownFacilityError { + #[allow(deprecated)] // Old versions of Rust require this. + fn description(&self) -> &str { + "unrecognized syslog facility name" + } +} + +#[test] +fn test_facility_from_str() { + assert_eq!(Facility::from_str("daemon"), Ok(Facility::Daemon)); + assert_eq!(Facility::from_str("foobar"), Err(UnknownFacilityError { name: "foobar".to_string() })); + assert_eq!(Facility::from_str("foobar").unwrap_err().to_string(), "unrecognized syslog facility name `foobar`"); +} diff --git a/src/format.rs b/src/format.rs new file mode 100644 index 0000000..b628367 --- /dev/null +++ b/src/format.rs @@ -0,0 +1,364 @@ +//! Ways to format syslog messages with structured data. +//! +//! See [`MsgFormat`] for more details. +//! +//! [`MsgFormat`]: trait.MsgFormat.html + +use slog::{self, KV, OwnedKVList, Record}; +use std::cell::Cell; +use std::fmt::{self, Debug, Display}; +use std::io; +use std::rc::Rc; +use std::sync::Arc; + +/// A way to format syslog messages with structured data. +/// +/// Syslog does not support structured log data. If Slog key-value pairs are to +/// be included with log messages, they must be included as part of the +/// message. Implementations of this trait determine if and how this will be +/// done. +pub trait MsgFormat: Debug { + /// Formats a log message and its key-value pairs into the given `Formatter`. + /// + /// Note that this method returns `slog::Result`, not `std::fmt::Result`. + /// The caller of this method is responsible for handling the error, + /// likely by storing it elsewhere and picking it up later. The free + /// function [`format`](fn.format.html) does just that. + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result; +} + +impl<'a, T: MsgFormat + ?Sized> MsgFormat for &'a T { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + MsgFormat::fmt(&**self, f, record, values) + } +} + +impl MsgFormat for Box { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + MsgFormat::fmt(&**self, f, record, values) + } +} + +impl MsgFormat for Rc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + MsgFormat::fmt(&**self, f, record, values) + } +} + +impl MsgFormat for Arc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + MsgFormat::fmt(&**self, f, record, values) + } +} + +// This helper structure provides a convenient way to implement +// `Display` with a closure. +struct ClosureAsDisplay fmt::Result>(F); +impl fmt::Result> Display for ClosureAsDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0(f) + } +} + +/// Formats a log message and its key-value pairs into the given writer using +/// the given message format. +/// +/// # Errors +/// +/// This method can fail if the [`MsgFormat::fmt`] method fails, as well as if +/// the `writer` encounters an I/O error. +/// +/// [`MsgFormat::fmt`]: trait.MsgFormat.html#tymethod.fmt +pub fn format(format: F, mut writer: W, record: &Record, values: &OwnedKVList) -> slog::Result<()> { + // If there is an error calling `format.fmt`, it will be stored here. We + // have to use `Cell` because the `Display::fmt` method doesn't get a + // mutable reference to `self`. + let result: Cell> = Cell::new(None); + + // Construct our `Display` implementation… + let displayable = ClosureAsDisplay(|f| { + // Do the formatting. + if let Err(e) = MsgFormat::fmt(&format, f, record, values) { + // If there's an error, smuggle it out. + result.set(Some(e)); + } + // Pretend to succeed, even if there was an error. The real error will + // be picked up later. + Ok(()) + }); + + // …and use it to write into the given writer. + let outer_result: io::Result<()> = write!(writer, "{}", displayable); + + // If there was an I/O error, fail with that. This takes precedence over + // the `result`, because if an I/O error happened, `result` probably + // contains a `slog::Error::Fmt` that resulted from the I/O error. + if let Err(e) = outer_result { + Err(slog::Error::Io(e)) + } + // If there was a formatter/serializer error other than one caused by I/O, + // fail with that. + else if let Some(e) = result.take() { + Err(e) + } + // No error. Yay! + else { + Ok(()) + } +} + +/// An implementation of [`MsgFormat`] that discards the key-value pairs and +/// logs only the [`msg`] part of a log [`Record`]. +/// +/// [`msg`]: https://docs.rs/slog/2/slog/struct.Record.html#method.msg +/// [`MsgFormat`]: trait.MsgFormat.html +/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html +#[derive(Clone, Copy, Debug, Default)] +pub struct BasicMsgFormat; +impl MsgFormat for BasicMsgFormat { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, _: &OwnedKVList) -> slog::Result { + write!(f, "{}", record.msg()).map_err(From::from) + } +} + +/// A [`MsgFormat`] implementation that calls a closure to perform the +/// formatting. +/// +/// This is meant to provide a convenient way to implement a custom +/// `MsgFormat`. +/// +/// # Example +/// +/// ``` +/// use slog_syslog::SyslogBuilder; +/// use slog_syslog::format::CustomMsgFormat; +/// +/// let drain = SyslogBuilder::new() +/// .format(CustomMsgFormat(|f, record, _| { +/// write!(f, "here's a message: {}", record.msg())?; +/// Ok(()) +/// })) +/// .build(); +/// ``` +/// +/// Note the use of the `?` operator. The closure is expected to return +/// `Result<(), slog::Error>`, not the `Result<(), std::fmt::Error>` that +/// `write!` returns. `slog::Error` does have a conversion from +/// `std::fmt::Error`, which the `?` operator will automatically perform. +/// +/// [`MsgFormat`]: trait.MsgFormat.html +pub struct CustomMsgFormat slog::Result>(pub T); +impl slog::Result> MsgFormat for CustomMsgFormat { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + self.0(f, record, values) + } +} +impl slog::Result> Debug for CustomMsgFormat { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("CustomMsgFormat").finish() + } +} + +/// Copies input to output, but escapes characters as prescribed by RFC 5424 for PARAM-VALUEs. +struct Rfc5424LikeValueEscaper(W); + +impl fmt::Write for Rfc5424LikeValueEscaper { + fn write_str(&mut self, mut s: &str) -> fmt::Result { + while let Some(index) = s.find(|c| c == '\\' || c == '"' || c == ']') { + if index != 0 { + self.0.write_str(&s[..index])?; + } + + // All three delimiters are ASCII characters, so this won't have bogus results. + self.write_char(s.as_bytes()[index] as char)?; + + if s.len() >= index { + s = &s[(index + 1)..]; + } + else { + s = &""; + break; + } + } + + if !s.is_empty() { + self.0.write_str(s)?; + } + + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + match c { + '\\' => self.0.write_str(r"\\"), + '"' => self.0.write_str("\\\""), + ']' => self.0.write_str("\\]"), + _ => write!(self.0, "{}", c) + } + } +} + +#[test] +fn test_rfc_5424_like_value_escaper() { + use std::iter; + + fn case(input: &str, expected_output: &str) { + let mut e = Rfc5424LikeValueEscaper(String::new()); + fmt::Write::write_str(&mut e, input).unwrap(); + assert_eq!(e.0, expected_output); + } + + // Test that each character is properly escaped. + for c in &['\\', '"', ']'] { + let ec = format!("\\{}", c); + + { + let input = format!("{}", c); + case(&*input, &*ec); + } + + for at_start_count in 0..=2 { + for at_mid_count in 0..=2 { + for at_end_count in 0..=2 { + // First, we assemble the input and expected output strings. + let mut input = String::new(); + let mut expected_output = String::new(); + + // Place the symbol(s) at the beginning of the strings. + input.extend(iter::repeat(c).take(at_start_count)); + expected_output.extend(iter::repeat(&*ec).take(at_start_count)); + + // First plain text. + input.push_str("foo"); + expected_output.push_str("foo"); + + // Middle symbol(s). + input.extend(iter::repeat(c).take(at_mid_count)); + expected_output.extend(iter::repeat(&*ec).take(at_mid_count)); + + // Second plain text. + input.push_str("bar"); + expected_output.push_str("bar"); + + // End symbol(s). + input.extend(iter::repeat(c).take(at_end_count)); + expected_output.extend(iter::repeat(&*ec).take(at_end_count)); + + // Finally, test this combination. + case(&*input, &*expected_output); + }}} + } + + case("", ""); + case("foo", "foo"); + case("[foo]", "[foo\\]"); + case("\\\"]", "\\\\\\\"\\]"); // \"] ⇒ \\\"\] +} + +/// An implementation of [`MsgFormat`] that formats the key-value pairs of a +/// log [`Record`] similarly to [RFC 5424]. +/// +/// # Not really RFC 5424 +/// +/// This does not actually generate conformant RFC 5424 STRUCTURED-DATA. The +/// differences are: +/// +/// * All key-value pairs are placed into a single SD-ELEMENT. +/// * The SD-ELEMENT does not contain an SD-ID, only SD-PARAMs. +/// * PARAM-NAMEs are encoded in UTF-8, not ASCII. +/// * Forbidden characters in PARAM-NAMEs are not filtered out, nor is an error +/// raised if a key contains such characters. +/// +/// # Example output +/// +/// Given a log message `Hello, world!`, where the key `key1` has the value +/// `value1` and `key2` has the value `value2`, the formatted message will be +/// `Hello, world! [key1="value1" key2="value2"]` (possibly with `key2` first +/// instead of `key1`). +/// +/// [`MsgFormat`]: trait.MsgFormat.html +/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html +/// [RFC 5424]: https://tools.ietf.org/html/rfc5424 +#[derive(Clone, Copy, Debug, Default)] +pub struct DefaultMsgFormat; +impl MsgFormat for DefaultMsgFormat { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + struct SerializerImpl<'a, 'b: 'a> { + f: &'a mut fmt::Formatter<'b>, + is_first_kv: bool, + } + + impl<'a, 'b> SerializerImpl<'a, 'b> { + fn new(f: &'a mut fmt::Formatter<'b>) -> Self { + Self { f, is_first_kv: true } + } + + fn finish(&mut self) -> slog::Result { + if !self.is_first_kv { + write!(self.f, "]")?; + } + Ok(()) + } + } + + impl<'a, 'b> slog::Serializer for SerializerImpl<'a, 'b> { + fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { + use std::fmt::Write; + + self.f.write_str(if self.is_first_kv {" ["} else {" "})?; + self.is_first_kv = false; + + // Write the key unaltered, but escape the value. + // + // RFC 5424 does not allow space, ']', '"', or '\' to + // appear in PARAM-NAMEs, and does not allow such + // characters to be escaped. + write!(self.f, "{}=\"", key)?; + write!(Rfc5424LikeValueEscaper(&mut self.f), "{}", val)?; + self.f.write_char('"')?; + Ok(()) + } + } + + write!(f, "{}", record.msg())?; + + { + let mut serializer = SerializerImpl::new(f); + + values.serialize(record, &mut serializer)?; + record.kv().serialize(record, &mut serializer)?; + serializer.finish()?; + } + + Ok(()) + } +} + +/// Makes sure the example output for `DefaultMsgFormat` is what it actually +/// generates. +#[test] +fn test_default_msg_format() { + use slog::Level; + + let mut buf = Vec::new(); + + format( + DefaultMsgFormat, + &mut buf, + &record!( + Level::Info, + "", + &format_args!("Hello, world!"), + b!("key1" => "value1") + ), + &o!("key2" => "value2").into(), + ).expect("formatting failed"); + + let result = String::from_utf8(buf).expect("invalid UTF-8"); + + assert!( + // The KVs' order is not well-defined, so they might get reversed. + result == "Hello, world! [key1=\"value1\" key2=\"value2\"]" || + result == "Hello, world! [key2=\"value2\" key1=\"value1\"]" + ); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..0144021 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,161 @@ +//! Slog [`Drain`] that sends logs to local syslog server. Unix-like platforms +//! only. Uses the [POSIX syslog API]. +//! +//! [`Drain`]: https://docs.rs/slog/2/slog/trait.Drain.html +//! [POSIX syslog API]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html +//! +//! # Example +//! +//! ``` +//! #[macro_use] extern crate slog; +//! # extern crate slog_syslog; +//! +//! use slog::Logger; +//! use slog_syslog::{Facility, SyslogBuilder}; +//! use std::ffi::CStr; +//! +//! let drain = SyslogBuilder::new() +//! .facility(Facility::User) +//! .ident(CStr::from_bytes_with_nul(b"example-app\0").unwrap()) +//! .build(); +//! +//! let logger = Logger::root(drain, o!()); +//! +//! info!(logger, "Hello, world! This is a test message from `slog-syslog`."); +//! ``` +//! +//! # Cargo features +//! +//! If the Cargo feature `serde` is enabled, syslog settings can be loaded from +//! a configuration file using [`config::SyslogConfig`]. +//! +//! [`config::SyslogConfig`]: config/struct.SyslogConfig.html +//! +//! # Concurrency issues +//! +//! POSIX doesn't support opening more than one connection to the syslog server +//! at a time. All [`SyslogBuilder` settings] except +//! [`format`][`SyslogBuilder::format`] are stored in global variables by the +//! platform libc, are overwritten whenever the POSIX `openlog` function is +//! called (which happens when a [`SyslogDrain`] is created), and are reset +//! whenever the POSIX `closelog` function is called (which may happen when a +//! [`SyslogDrain`] is dropped). +//! +//! For this reason, the following rules should be followed: +//! +//! * Libraries should not construct a [`SyslogDrain`] or otherwise call +//! `openlog` or `closelog` unless specifically told to do so by the main +//! application. +//! * An application that uses this crate should not cause `openlog` or +//! `closelog` to be called from anywhere else. +//! * An application should not construct more than one [`SyslogDrain`] at the +//! same time, except when constructing a new [`SyslogDrain`] that +//! is to replace an old one (for instance, if the application is reloading +//! its configuration files and reinitializing its logging pipeline). +//! +//! Failure to abide by these rules may result in `closelog` being called at +//! the wrong time. This will cause [`SyslogBuilder` settings] (except +//! [`format`][`SyslogBuilder::format`]) to be reset, and there may be a delay +//! in processing the next log message after that (because the connection to +//! the syslog server, if applicable, must be reopened). +//! +//! [`SyslogBuilder::format`]: struct.SyslogBuilder.html#method.format +//! [`SyslogBuilder` settings]: struct.SyslogBuilder.html#impl +//! [`SyslogDrain`]: struct.SyslogDrain.html + +// TODO: Some systems (including OpenBSD and Android) have reentrant versions +// of the POSIX syslog functions. These systems *do* support opening multiple +// connections to syslog, and therefore do not suffer from the above +// concurrency issues. Perhaps this crate should use the reentrant syslog API +// on those platforms. + +// # Design and rationale +// +// (This section is not part of the documentation for this crate. It's only a +// source code comment.) +// +// This crate uses the POSIX syslog API to submit log entries to the local +// syslogd. This is unlike the `syslog` crate, which connects to `/dev/log` +// or `/var/run/log` directly. The reasons for this approach, despite the above +// drawbacks, are as follows. +// +// ## Portability +// +// POSIX only specifies the `syslog` function and related functions. +// +// POSIX does not specify that a Unix-domain socket is used for submitting log +// messages to syslogd, nor the socket's path, nor the protocol used on that +// socket. The path of the socket is different on different systems: +// +// * `/dev/log` – original BSD, OpenBSD, Linux +// * `/var/run/log` – FreeBSD and NetBSD (but on Linux with systemd, this +// is a folder) +// * `/var/run/syslog` – Darwin/macOS +// +// The protocol spoken on the socket is not formally specified. It is +// whatever the system's `syslog` function writes to it, which may of course +// vary between systems. It is typically different from IETF RFCs 3164 and +// 5424. +// +// The OpenBSD kernel has a dedicated system call for submitting log messages. +// `/dev/log` is still available, but not preferred. +// +// On macOS, the `syslog` function submits log entries to the Apple System Log +// service. BSD-style log messages are accepted on `/var/run/syslog`, but that +// is not preferred. +// +// ## Reliability +// +// On every platform that has a `syslog` function, it is battle-tested and +// very definitely works. +// +// ## Simplicity +// +// Even in “classic” implementations of the POSIX `syslog` function, there are +// a number of details that it keeps track of: +// +// * Opening the socket +// * Reopening the socket when necessary +// * Formatting log messages for consumption by syslogd +// * Determining the name of the process, when none is specified by the +// application +// +// By calling the POSIX function, we avoid needing to reimplement all this in +// Rust. + +#![cfg(unix)] +#![warn(missing_docs)] + +extern crate libc; + +#[macro_use] +extern crate lazy_static; + +#[cfg(feature = "serde")] +#[macro_use] +extern crate serde; + +#[cfg_attr(test, macro_use)] +extern crate slog; + +#[cfg(all(test, feature = "serde"))] +extern crate toml; + +mod builder; +pub use builder::*; + +#[cfg(feature = "serde")] +pub mod config; + +mod drain; +pub use drain::*; + +mod facility; +pub use facility::*; + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; + +pub mod format; diff --git a/src/mock.rs b/src/mock.rs new file mode 100644 index 0000000..e7ffce0 --- /dev/null +++ b/src/mock.rs @@ -0,0 +1,89 @@ +//! Mocks for the POSIX `syslog` API. +//! +//! The mock `syslog` function here is a bit different from the real one. It +//! takes exactly three parameters, whereas the real one takes two or more. +//! This works for our purposes because this crate always calls it with exactly +//! three parameters anyway. + +use libc::{c_char, c_int}; +use std::ffi::CStr; +use std::mem; +use std::panic::{AssertUnwindSafe, catch_unwind, resume_unwind}; +use std::sync::{Condvar, Mutex, MutexGuard}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum Event { + OpenLog { + ident: String, + flags: c_int, + facility: c_int, + }, + CloseLog, + SysLog { + priority: c_int, + message_f: String, + message: String, + }, + DropOwnedIdent(String), +} + +lazy_static! { + static ref EVENTS: Mutex> = Mutex::new(Vec::new()); + static ref EVENTS_CV: Condvar = Condvar::new(); + static ref TESTING: Mutex<()> = Mutex::new(()); +} + +pub fn testing(f: impl FnOnce() -> T) -> (T, Vec) { + let _locked = TESTING.lock().unwrap(); + + let result = catch_unwind(AssertUnwindSafe(f)); + let events = take_events(); + + match result { + Ok(ok) => (ok, events), + Err(panicked) => resume_unwind(panicked), + } +} + +pub fn take_events() -> Vec { + let mut events: MutexGuard> = EVENTS.lock().unwrap(); + mem::replace(&mut *events, Vec::new()) +} + +pub fn push_event(event: Event) { + let mut events: MutexGuard> = EVENTS.lock().unwrap(); + events.push(event); + EVENTS_CV.notify_all(); +} + +pub fn wait_for_event_matching(matching: impl Fn(&Event) -> bool) { + let mut events: MutexGuard> = EVENTS.lock().unwrap(); + + while !events.iter().any(&matching) { + events = EVENTS_CV.wait(events).unwrap(); + } +} + +pub unsafe extern "C" fn openlog(ident: *const c_char, logopt: c_int, facility: c_int) { + push_event(Event::OpenLog { + ident: string_from_ptr(ident), + flags: logopt, + facility, + }); +} + +pub unsafe extern "C" fn closelog() { + push_event(Event::CloseLog); +} + +pub unsafe extern "C" fn syslog(priority: c_int, message_f: *const c_char, message: *const c_char) { + push_event(Event::SysLog { + priority, + message_f: string_from_ptr(message_f), + message: string_from_ptr(message), + }); +} + +pub unsafe fn string_from_ptr(ptr: *const c_char) -> String { + String::from(CStr::from_ptr(ptr).to_string_lossy()) +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..9a6c774 --- /dev/null +++ b/src/tests.rs @@ -0,0 +1,98 @@ +use Facility; +use format::CustomMsgFormat; +use libc; +use mock; +use slog::{self, Logger}; +use std::borrow::Cow; +use std::ffi::CStr; +use SyslogBuilder; + +#[test] +fn test_log() { + let ((), events) = mock::testing(|| { + { + let tmp_logger = Logger::root_typed(SyslogBuilder::new() + .ident_str("hello") + .log_ndelay() + .log_odelay() + .log_pid() + .build(), o!()); + + warn!(tmp_logger, "Constructed a temporary logger."); + + // The logger will be dropped at this point, which should result in + // a `closelog` call. + } + + let logger = Logger::root_typed(SyslogBuilder::new() + .facility(Facility::Local0) + .ident_str("example-app") + .build(), o!()); + + info!(logger, "Hello, world! This is a test message from `slog-syslog`."; "test" => "message"); + + mock::wait_for_event_matching(|event| match event { + mock::Event::SysLog { message, .. } => message.contains("This is a test message"), + _ => false, + }); + + let logger2 = Logger::root_typed(SyslogBuilder::new() + .facility(Facility::Local1) + .ident(Cow::Borrowed(CStr::from_bytes_with_nul(b"logger2\0").unwrap())) + .format(CustomMsgFormat(|_, _, _| Err(slog::Error::Other))) + .build(), o!()); + + info!(logger2, "Message from second logger while first still active."; "key" => "value"); + + mock::wait_for_event_matching(|event| match event { + mock::Event::SysLog { message, .. } => message == &slog::Error::Other.to_string(), + _ => false, + }); + }); + + let expected_events = vec![ + mock::Event::OpenLog { + facility: libc::LOG_USER, + flags: libc::LOG_ODELAY | libc::LOG_PID, + ident: "hello".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_WARNING, + message_f: "%s".to_string(), + message: "Constructed a temporary logger.".to_string(), + }, + // This logger will `closelog` when dropped, because it has to in order + // to free its `ident` string. + mock::Event::CloseLog, + mock::Event::DropOwnedIdent("hello".to_string()), + mock::Event::OpenLog { + facility: libc::LOG_LOCAL0, + flags: 0, + ident: "example-app".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_INFO, + message_f: "%s".to_string(), + message: "Hello, world! This is a test message from `slog-syslog`. [test=\"message\"]".to_string() + }, + mock::Event::OpenLog { + facility: libc::LOG_LOCAL1, + flags: 0, + ident: "logger2".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_INFO, + message_f: "%s".to_string(), + message: "Message from second logger while first still active.".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_ERR, + message_f: "Error fully formatting the previous log message: %s".to_string(), + message: slog::Error::Other.to_string(), + }, + mock::Event::DropOwnedIdent("example-app".to_string()), + // No `CloseLog` for `logger2` because it doesn't own its `ident`. + ]; + + assert!(events == expected_events, "events didn't match\ngot: {:#?}\nexpected: {:#?}", events, expected_events); +} From dee8f6ffb3782e51eb8848de852e2a09f9b07d10 Mon Sep 17 00:00:00 2001 From: Aron Wieck Date: Wed, 29 Jul 2020 10:26:53 +0200 Subject: [PATCH 2/3] Additional configuration option log_priority to log all messages with a given syslog priority --- examples/syslog-unix.rs | 3 ++- src/builder.rs | 14 +++++++++++++- src/config.rs | 11 ++++++++++- src/drain.rs | 31 ++++++++++++++++++++----------- src/tests.rs | 35 +++++++++++++++++++++++++++++++++-- 5 files changed, 78 insertions(+), 16 deletions(-) diff --git a/examples/syslog-unix.rs b/examples/syslog-unix.rs index d4bb0d7..e3428da 100644 --- a/examples/syslog-unix.rs +++ b/examples/syslog-unix.rs @@ -3,9 +3,10 @@ extern crate slog; extern crate slog_syslog; use slog_syslog::{Facility, SyslogBuilder}; +use slog::Level; fn main() { - let syslog = SyslogBuilder::new().facility(Facility::User).build(); + let syslog = SyslogBuilder::new().facility(Facility::User).log_priority(Level::Error).build(); let root = slog::Logger::root(syslog, o!()); info!(root, "Starting"); diff --git a/src/builder.rs b/src/builder.rs index a226997..ad61fcb 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,9 +1,10 @@ -use Facility; +use ::{Facility, get_priority}; use format::{DefaultMsgFormat, MsgFormat}; use libc; use SyslogDrain; use std::borrow::Cow; use std::ffi::{CStr, CString}; +use slog::Level; /// Builds a [`SyslogDrain`]. /// @@ -19,6 +20,7 @@ pub struct SyslogBuilder { pub(crate) facility: Facility, pub(crate) ident: Option>, pub(crate) option: libc::c_int, + pub(crate) log_priority: libc::c_int, pub(crate) format: F, } @@ -28,6 +30,7 @@ impl Default for SyslogBuilder { facility: Facility::default(), ident: None, option: 0, + log_priority: 0, format: DefaultMsgFormat, } } @@ -237,6 +240,14 @@ impl SyslogBuilder { self } + /// Log all messages with the given syslog priority + #[inline] + pub fn log_priority(mut self, log_priority: Level) -> Self { + self.log_priority = get_priority(log_priority); + self + } + + /// Set a format for log messages and structured data. /// /// The default is [`DefaultMsgFormat`]. @@ -258,6 +269,7 @@ impl SyslogBuilder { facility: self.facility, ident: self.ident, option: self.option, + log_priority: self.log_priority, format, } } diff --git a/src/config.rs b/src/config.rs index accaadf..31b4190 100644 --- a/src/config.rs +++ b/src/config.rs @@ -6,7 +6,7 @@ use Facility; use format::{BasicMsgFormat, DefaultMsgFormat, MsgFormat}; -use slog::{self, OwnedKVList, Record}; +use slog::{self, OwnedKVList, Record, Level}; use std::borrow::Cow; use std::ffi::CStr; use std::fmt; @@ -92,6 +92,9 @@ pub struct SyslogConfig { /// in garbled output. pub log_perror: bool, + /// Log all messages with the given priority + pub log_priority: Option, + #[serde(skip)] __non_exhaustive: (), } @@ -129,6 +132,11 @@ impl SyslogConfig { false => b, }; + let b = match self.log_priority { + Some(priority) => b.log_priority(priority), + None => b, + }; + b } @@ -147,6 +155,7 @@ impl Default for SyslogConfig { log_pid: false, log_delay: None, log_perror: false, + log_priority: None, __non_exhaustive: (), } } diff --git a/src/drain.rs b/src/drain.rs index 75df74f..289f90e 100644 --- a/src/drain.rs +++ b/src/drain.rs @@ -67,6 +67,9 @@ pub struct SyslogDrain { /// necessary) when this `SyslogDrain` is dropped. unique_ident: Option>, + /// Log all messages with the given priority + log_priority: libc::c_int, + /// The format for log messages. format: F, } @@ -135,6 +138,7 @@ impl SyslogDrain { SyslogDrain { unique_ident, + log_priority: builder.log_priority, format: builder.format, } } @@ -225,17 +229,7 @@ impl Drain for SyslogDrain { let mut tl_buf = &mut *tl_buf_mut; // Figure out the priority. - let priority: c_int = match record.level() { - Level::Critical => libc::LOG_CRIT, - Level::Error => libc::LOG_ERR, - Level::Warning => libc::LOG_WARNING, - Level::Debug | Level::Trace => libc::LOG_DEBUG, - - // `slog::Level` isn't non-exhaustive, so adding any more levels - // would be a breaking change. That is highly unlikely to ever - // happen. Still, we'll handle the possibility here, just in case. - _ => libc::LOG_INFO - }; + let priority = if self.log_priority > 0 { self.log_priority } else { get_priority(record.level()) }; // Format the message. let fmt_err = format(&self.format, &mut tl_buf, record, values).err(); @@ -312,3 +306,18 @@ fn assert_format_success(_result: io::Result<()>) { #[cfg(debug)] _result.expect("unexpected formatting error"); } + + +pub(crate) fn get_priority(level: Level) -> c_int { + match level { + Level::Critical => libc::LOG_CRIT, + Level::Error => libc::LOG_ERR, + Level::Warning => libc::LOG_WARNING, + Level::Debug | Level::Trace => libc::LOG_DEBUG, + + // `slog::Level` isn't non-exhaustive, so adding any more levels + // would be a breaking change. That is highly unlikely to ever + // happen. Still, we'll handle the possibility here, just in case. + _ => libc::LOG_INFO + } +} \ No newline at end of file diff --git a/src/tests.rs b/src/tests.rs index 9a6c774..1f44cba 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -2,7 +2,7 @@ use Facility; use format::CustomMsgFormat; use libc; use mock; -use slog::{self, Logger}; +use slog::{self, Logger, Level}; use std::borrow::Cow; use std::ffi::CStr; use SyslogBuilder; @@ -48,6 +48,21 @@ fn test_log() { mock::Event::SysLog { message, .. } => message == &slog::Error::Other.to_string(), _ => false, }); + + + let logger3 = Logger::root_typed(SyslogBuilder::new() + .facility(Facility::Local1) + .log_priority(Level::Error) + .ident(Cow::Borrowed(CStr::from_bytes_with_nul(b"logger3\0").unwrap())) + .format(CustomMsgFormat(|_, _, _| Err(slog::Error::Other))) + .build(), o!()); + + info!(logger3, "Message from third logger while first still active."; "key" => "value"); + + mock::wait_for_event_matching(|event| match event { + mock::Event::SysLog { message, .. } => message == &slog::Error::Other.to_string(), + _ => false, + }); }); let expected_events = vec![ @@ -90,8 +105,24 @@ fn test_log() { message_f: "Error fully formatting the previous log message: %s".to_string(), message: slog::Error::Other.to_string(), }, + + mock::Event::OpenLog { + facility: libc::LOG_LOCAL1, + flags: 0, + ident: "logger3".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_ERR, + message_f: "%s".to_string(), + message: "Message from third logger while first still active.".to_string(), + }, + mock::Event::SysLog { + priority: libc::LOG_ERR, + message_f: "Error fully formatting the previous log message: %s".to_string(), + message: slog::Error::Other.to_string(), + }, mock::Event::DropOwnedIdent("example-app".to_string()), - // No `CloseLog` for `logger2` because it doesn't own its `ident`. + // No `CloseLog` for `logger2` and `logger3` because it doesn't own its `ident`. ]; assert!(events == expected_events, "events didn't match\ngot: {:#?}\nexpected: {:#?}", events, expected_events); From 4270074ff4de8973f43eaa6597820d057b00b547 Mon Sep 17 00:00:00 2001 From: argv-minus-one Date: Wed, 5 Aug 2020 01:31:16 -0700 Subject: [PATCH 3/3] Rename `MsgFormat` to `Adapter`. Allow control of syslog priorities on a per-message basis. Thanks to @eun-ice for the idea and initial implementation. --- Cargo.toml | 2 +- examples/syslog-unix.rs | 10 +- src/adapter.rs | 622 ++++++++++++++++++++++++++++++++++++++++ src/builder.rs | 106 +++++-- src/config.rs | 218 +++++++++++--- src/drain.rs | 47 +-- src/format.rs | 364 ----------------------- src/level.rs | 207 +++++++++++++ src/lib.rs | 14 +- src/priority.rs | 215 ++++++++++++++ src/tests.rs | 27 +- 11 files changed, 1350 insertions(+), 482 deletions(-) create mode 100644 src/adapter.rs delete mode 100644 src/format.rs create mode 100644 src/level.rs create mode 100644 src/priority.rs diff --git a/Cargo.toml b/Cargo.toml index 7bb045b..8b87818 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,7 @@ readme = "README.md" [dependencies] lazy_static = "1" libc = "0.2" -serde = { version = "1", optional = true, features = ["derive"] } +serde = { version = "1.0.85", optional = true, features = ["derive"] } slog = "^2.1.1" [dev-dependencies] diff --git a/examples/syslog-unix.rs b/examples/syslog-unix.rs index e3428da..2b43be1 100644 --- a/examples/syslog-unix.rs +++ b/examples/syslog-unix.rs @@ -2,11 +2,19 @@ extern crate slog; extern crate slog_syslog; +use slog_syslog::adapter::{Adapter, DefaultAdapter}; use slog_syslog::{Facility, SyslogBuilder}; use slog::Level; fn main() { - let syslog = SyslogBuilder::new().facility(Facility::User).log_priority(Level::Error).build(); + let syslog = SyslogBuilder::new() + .facility(Facility::User) + .adapter(DefaultAdapter.with_priority(|record, values| match record.level() { + Level::Info => slog_syslog::Level::Notice.into(), + _ => DefaultAdapter.priority(record, values), + })) + .build(); + let root = slog::Logger::root(syslog, o!()); info!(root, "Starting"); diff --git a/src/adapter.rs b/src/adapter.rs new file mode 100644 index 0000000..f233c7f --- /dev/null +++ b/src/adapter.rs @@ -0,0 +1,622 @@ +//! Customize the conversion of [`slog::Record`]s to syslog messages. +//! +//! See [`Adapter`] for more details. +//! +//! [`Adapter`]: trait.Adapter.html +//! [`slog::Record`]: https://docs.rs/slog/2/slog/struct.Record.html + +use ::{Level, Priority}; +use slog::{self, KV, OwnedKVList, Record}; +use std::cell::Cell; +use std::fmt::{self, Debug, Display}; +use std::io; +use std::rc::Rc; +use std::sync::Arc; + +/// Converts [`slog::Record`]s to syslog messages. +/// +/// An `Adapter` has two responsibilities: +/// +/// 1. Format structured log data into a syslog message. +/// 2. Determine the message's syslog [priority]. +/// +/// # Structured Data +/// +/// Syslog does not support structured log data. If Slog key-value pairs are to +/// be included with log messages, they must be included as part of the +/// message. Implementations of this trait's `fmt` method determine if and +/// how this will be done. +/// +/// # Priority +/// +/// Each message sent to syslog has a “[priority]”, which consists of a +/// required [severity level] and an optional [facility]. This doesn't match +/// [`slog::Level`], so an implementation of the [`priority`] method of this +/// trait is used to choose a priority for each [`slog::Record`]. +/// +/// [facility]: ../enum.Facility.html +/// [priority]: ../struct.Priority.html +/// [`priority`]: #method.priority +/// [severity level]: ../enum.Level.html +/// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html +/// [`slog::Record`]: https://docs.rs/slog/2/slog/struct.Record.html +pub trait Adapter: Debug { + /// Formats a log message and its key-value pairs into the given `Formatter`. + /// + /// Note that this method returns `slog::Result`, not `std::fmt::Result`. + /// The caller of this method is responsible for handling the error, + /// likely by storing it elsewhere and picking it up later. The free + /// function [`format`](fn.format.html) does just that. + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result; + + /// Creates a new `Adapter` based on this one, but whose `fmt` method + /// delegates to the provided closure. + /// + /// # Example + /// + /// This formatting function simply prepends `here's a message: ` to each + /// log message: + /// + /// ``` + /// use slog_syslog::adapter::{Adapter, DefaultAdapter}; + /// use slog_syslog::SyslogBuilder; + /// + /// let drain = SyslogBuilder::new() + /// .adapter(DefaultAdapter.with_fmt(|f, record, _| { + /// write!(f, "here's a message: {}", record.msg())?; + /// Ok(()) + /// })) + /// .build(); + /// ``` + /// + /// The [`SyslogBuilder::format`] method is a convenient shorthand: + /// + /// ``` + /// use slog_syslog::SyslogBuilder; + /// + /// let drain = SyslogBuilder::new() + /// .format(|f, record, _| { + /// write!(f, "here's a message: {}", record.msg())?; + /// Ok(()) + /// }) + /// .build(); + /// ``` + /// + /// Note the use of the `?` operator. The closure is expected to return + /// `Result<(), slog::Error>`, not the `Result<(), std::fmt::Error>` that + /// `write!` returns. `slog::Error` does have a conversion from + /// `std::fmt::Error`, which the `?` operator will automatically perform. + /// + /// [`SyslogBuilder::format`]: ../struct.SyslogBuilder.html#method.format + fn with_fmt(self, fmt_fn: F) -> WithFormat + where + Self: Sized, + F: Fn(&mut fmt::Formatter, &Record, &OwnedKVList) -> slog::Result, + { + WithFormat { + fmt_fn, + inner: self, + } + } + + /// Examines a log message and determines its syslog [`Priority`]. + /// + /// The default implementation calls [`Level::from_slog`], which maps + /// [`slog::Level`]s as follows: + /// + /// * [`Critical`][slog critical] ⇒ [`Crit`][syslog crit] + /// * [`Error`][slog error] ⇒ [`Err`][syslog err] + /// * [`Warning`][slog warning] ⇒ [`Warning`][syslog warning] + /// * [`Info`][slog info] ⇒ [`Info`][syslog info] + /// * [`Debug`][slog debug] ⇒ [`Debug`][syslog debug] + /// * [`Trace`][slog trace] ⇒ [`Debug`][syslog debug] + /// + /// [`Level::from_slog`]: ../enum.Level.html#method.from_slog + /// [`Priority`]: ../struct.Priority.html + /// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html + /// [slog critical]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Critical + /// [slog error]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Error + /// [slog warning]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Warning + /// [slog info]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Info + /// [slog debug]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Debug + /// [slog trace]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Trace + /// [syslog crit]: ../enum.Level.html#variant.Crit + /// [syslog err]: ../enum.Level.html#variant.Err + /// [syslog warning]: ../enum.Level.html#variant.Warning + /// [syslog info]: ../enum.Level.html#variant.Info + /// [syslog debug]: ../enum.Level.html#variant.Debug + #[allow(unused_variables)] + fn priority(&self, record: &Record, values: &OwnedKVList) -> Priority { + Level::from_slog(record.level()).into() + } + + /// Creates a new `Adapter` based on this one, but whose `priority` method + /// delegates to the provided closure. + /// + /// If you want to use the default formatting and only want to control + /// priorities, use this method. + /// + /// # Example + /// + /// ## Force all messages to [`Level::Err`] + /// + /// This uses the default message formatting, but makes all syslog messages + /// be [`Level::Err`]: + /// + /// ``` + /// use slog_syslog::adapter::{Adapter, DefaultAdapter}; + /// use slog_syslog::SyslogBuilder; + /// use slog_syslog::Level; + /// + /// let drain = SyslogBuilder::new() + /// .adapter(DefaultAdapter.with_priority(|record, _| { + /// Level::Err.into() + /// })) + /// .build(); + /// ``` + /// + /// The [`SyslogBuilder::priority`] method is a convenient shorthand: + /// + /// ``` + /// use slog_syslog::SyslogBuilder; + /// use slog_syslog::Level; + /// + /// let drain = SyslogBuilder::new() + /// .priority(|record, _| { + /// Level::Err.into() + /// }) + /// .build(); + /// ``` + /// + /// Notice the use of `into()`. [`Level`] can be converted directly into + /// [`Priority`]. + /// + /// ## Override level and facility + /// + /// In this example, [`slog::Level::Info`] messages from the `my_app::mail` + /// module are logged as [`Level::Notice`] instead of the default + /// [`Level::Info`], and all messages from that module are logged with a + /// different facility: + /// + /// ``` + /// # extern crate slog; + /// # extern crate slog_syslog; + /// use slog_syslog::{Facility, Level, Priority, SyslogBuilder}; + /// + /// let drain = SyslogBuilder::new() + /// .facility(Facility::Daemon) + /// .priority(|record, _| { + /// Priority::new( + /// match record.level() { + /// slog::Level::Info => Level::Notice, + /// other => Level::from_slog(other), + /// }, + /// match record.module() { + /// "my_app::mail" => Some(Facility::Mail), + /// _ => None, + /// }, + /// ) + /// }) + /// .build(); + /// ``` + /// + /// [`Level`]: ../enum.Level.html + /// [`Level::Err`]: ../enum.Level.html#variant.Err + /// [`Level::Info`]: ../enum.Level.html#variant.Info + /// [`Level::Notice`]: ../enum.Level.html#variant.Notice + /// [`Priority`]: ../struct.Priority.html + /// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html + /// [`slog::Level::Info`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Info + /// [`SyslogBuilder::priority`]: ../struct.SyslogBuilder.html#method.priority + fn with_priority

(self, priority_fn: P) -> WithPriority + where + Self: Sized, + P: Fn(&Record, &OwnedKVList) -> Priority, + { + WithPriority { + inner: self, + priority_fn, + } + } +} + +impl<'a, T: Adapter + ?Sized> Adapter for &'a T { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + Adapter::fmt(&**self, f, record, values) + } +} + +impl Adapter for Box { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + Adapter::fmt(&**self, f, record, values) + } +} + +impl Adapter for Rc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + Adapter::fmt(&**self, f, record, values) + } +} + +impl Adapter for Arc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + Adapter::fmt(&**self, f, record, values) + } +} + +// This helper structure provides a convenient way to implement +// `Display` with a closure. +struct ClosureAsDisplay fmt::Result>(A); +impl fmt::Result> Display for ClosureAsDisplay { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + self.0(f) + } +} + +/// Formats a log message and its key-value pairs into the given writer using +/// the given adapter. +/// +/// # Errors +/// +/// This method can fail if the [`Adapter::fmt`] method fails, as well as if +/// the `writer` encounters an I/O error. +/// +/// [`Adapter::fmt`]: trait.Adapter.html#tymethod.fmt +pub fn format(adapter: A, mut writer: W, record: &Record, values: &OwnedKVList) -> slog::Result<()> { + // If there is an error calling `adapter.fmt`, it will be stored here. We + // have to use `Cell` because the `Display::fmt` method doesn't get a + // mutable reference to `self`. + let result: Cell> = Cell::new(None); + + // Construct our `Display` implementation… + let displayable = ClosureAsDisplay(|f| { + // Do the formatting. + if let Err(e) = Adapter::fmt(&adapter, f, record, values) { + // If there's an error, smuggle it out. + result.set(Some(e)); + } + // Pretend to succeed, even if there was an error. The real error will + // be picked up later. + Ok(()) + }); + + // …and use it to write into the given writer. + let outer_result: io::Result<()> = write!(writer, "{}", displayable); + + // If there was an I/O error, fail with that. This takes precedence over + // the `result`, because if an I/O error happened, `result` probably + // contains a `slog::Error::Fmt` that resulted from the I/O error. + if let Err(e) = outer_result { + Err(slog::Error::Io(e)) + } + // If there was a formatter/serializer error other than one caused by I/O, + // fail with that. + else if let Some(e) = result.take() { + Err(e) + } + // No error. Yay! + else { + Ok(()) + } +} + +/// An implementation of [`Adapter`] that discards the key-value pairs and +/// logs only the [`msg`] part of a log [`Record`]. +/// +/// [`msg`]: https://docs.rs/slog/2/slog/struct.Record.html#method.msg +/// [`Adapter`]: trait.Adapter.html +/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html +#[derive(Clone, Copy, Debug, Default)] +pub struct BasicAdapter; +impl Adapter for BasicAdapter { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, _: &OwnedKVList) -> slog::Result { + write!(f, "{}", record.msg()).map_err(From::from) + } +} + +/// Copies input to output, but escapes characters as prescribed by RFC 5424 for PARAM-VALUEs. +struct Rfc5424LikeValueEscaper(W); + +impl fmt::Write for Rfc5424LikeValueEscaper { + fn write_str(&mut self, mut s: &str) -> fmt::Result { + while let Some(index) = s.find(|c| c == '\\' || c == '"' || c == ']') { + if index != 0 { + self.0.write_str(&s[..index])?; + } + + // All three delimiters are ASCII characters, so this won't have bogus results. + self.write_char(s.as_bytes()[index] as char)?; + + if s.len() >= index { + s = &s[(index + 1)..]; + } + else { + s = &""; + break; + } + } + + if !s.is_empty() { + self.0.write_str(s)?; + } + + Ok(()) + } + + fn write_char(&mut self, c: char) -> fmt::Result { + match c { + '\\' => self.0.write_str(r"\\"), + '"' => self.0.write_str("\\\""), + ']' => self.0.write_str("\\]"), + _ => write!(self.0, "{}", c) + } + } +} + +#[test] +fn test_rfc_5424_like_value_escaper() { + use std::iter; + + fn case(input: &str, expected_output: &str) { + let mut e = Rfc5424LikeValueEscaper(String::new()); + fmt::Write::write_str(&mut e, input).unwrap(); + assert_eq!(e.0, expected_output); + } + + // Test that each character is properly escaped. + for c in &['\\', '"', ']'] { + let ec = format!("\\{}", c); + + { + let input = format!("{}", c); + case(&*input, &*ec); + } + + for at_start_count in 0..=2 { + for at_mid_count in 0..=2 { + for at_end_count in 0..=2 { + // First, we assemble the input and expected output strings. + let mut input = String::new(); + let mut expected_output = String::new(); + + // Place the symbol(s) at the beginning of the strings. + input.extend(iter::repeat(c).take(at_start_count)); + expected_output.extend(iter::repeat(&*ec).take(at_start_count)); + + // First plain text. + input.push_str("foo"); + expected_output.push_str("foo"); + + // Middle symbol(s). + input.extend(iter::repeat(c).take(at_mid_count)); + expected_output.extend(iter::repeat(&*ec).take(at_mid_count)); + + // Second plain text. + input.push_str("bar"); + expected_output.push_str("bar"); + + // End symbol(s). + input.extend(iter::repeat(c).take(at_end_count)); + expected_output.extend(iter::repeat(&*ec).take(at_end_count)); + + // Finally, test this combination. + case(&*input, &*expected_output); + }}} + } + + case("", ""); + case("foo", "foo"); + case("[foo]", "[foo\\]"); + case("\\\"]", "\\\\\\\"\\]"); // \"] ⇒ \\\"\] +} + +/// An implementation of [`Adapter`] that formats the key-value pairs of a +/// log [`Record`] similarly to [RFC 5424]. +/// +/// # Not really RFC 5424 +/// +/// This does not actually generate conformant RFC 5424 STRUCTURED-DATA. The +/// differences are: +/// +/// * All key-value pairs are placed into a single SD-ELEMENT. +/// * The SD-ELEMENT does not contain an SD-ID, only SD-PARAMs. +/// * PARAM-NAMEs are encoded in UTF-8, not ASCII. +/// * Forbidden characters in PARAM-NAMEs are not filtered out, nor is an error +/// raised if a key contains such characters. +/// +/// # Example output +/// +/// Given a log message `Hello, world!`, where the key `key1` has the value +/// `value1` and `key2` has the value `value2`, the formatted message will be +/// `Hello, world! [key1="value1" key2="value2"]` (possibly with `key2` first +/// instead of `key1`). +/// +/// [`Adapter`]: trait.Adapter.html +/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html +/// [RFC 5424]: https://tools.ietf.org/html/rfc5424 +#[derive(Clone, Copy, Debug, Default)] +pub struct DefaultAdapter; +impl Adapter for DefaultAdapter { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + struct SerializerImpl<'a, 'b: 'a> { + f: &'a mut fmt::Formatter<'b>, + is_first_kv: bool, + } + + impl<'a, 'b> SerializerImpl<'a, 'b> { + fn new(f: &'a mut fmt::Formatter<'b>) -> Self { + Self { f, is_first_kv: true } + } + + fn finish(&mut self) -> slog::Result { + if !self.is_first_kv { + write!(self.f, "]")?; + } + Ok(()) + } + } + + impl<'a, 'b> slog::Serializer for SerializerImpl<'a, 'b> { + fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { + use std::fmt::Write; + + self.f.write_str(if self.is_first_kv {" ["} else {" "})?; + self.is_first_kv = false; + + // Write the key unaltered, but escape the value. + // + // RFC 5424 does not allow space, ']', '"', or '\' to + // appear in PARAM-NAMEs, and does not allow such + // characters to be escaped. + write!(self.f, "{}=\"", key)?; + write!(Rfc5424LikeValueEscaper(&mut self.f), "{}", val)?; + self.f.write_char('"')?; + Ok(()) + } + } + + write!(f, "{}", record.msg())?; + + { + let mut serializer = SerializerImpl::new(f); + + values.serialize(record, &mut serializer)?; + record.kv().serialize(record, &mut serializer)?; + serializer.finish()?; + } + + Ok(()) + } +} + +/// Makes sure the example output for `DefaultAdapter` is what it actually +/// generates. +#[test] +fn test_default_adapter_fmt() { + use slog::Level; + + let mut buf = Vec::new(); + + format( + DefaultAdapter, + &mut buf, + &record!( + Level::Info, + "", + &format_args!("Hello, world!"), + b!("key1" => "value1") + ), + &o!("key2" => "value2").into(), + ).expect("formatting failed"); + + let result = String::from_utf8(buf).expect("invalid UTF-8"); + + assert!( + // The KVs' order is not well-defined, so they might get reversed. + result == "Hello, world! [key1=\"value1\" key2=\"value2\"]" || + result == "Hello, world! [key2=\"value2\" key1=\"value1\"]" + ); +} + +/// An [`Adapter`] implementation that calls a closure to perform custom +/// formatting. +/// +/// # Example +/// +/// ``` +/// use slog_syslog::adapter::{Adapter, DefaultAdapter}; +/// use slog_syslog::SyslogBuilder; +/// +/// let drain = SyslogBuilder::new() +/// .format(|f, record, _| { +/// write!(f, "here's a message: {}", record.msg())?; +/// Ok(()) +/// }) +/// .build(); +/// ``` +/// +/// Note the use of the `?` operator. The closure is expected to return +/// `Result<(), slog::Error>`, not the `Result<(), std::fmt::Error>` that +/// `write!` returns. `slog::Error` does have a conversion from +/// `std::fmt::Error`, which the `?` operator will automatically perform. +/// +/// [`Adapter`]: trait.Adapter.html +#[derive(Clone, Copy)] +pub struct WithFormat +where + A: Adapter, + F: Fn(&mut fmt::Formatter, &Record, &OwnedKVList) -> slog::Result, +{ + fmt_fn: F, + inner: A, +} + +impl Adapter for WithFormat +where + A: Adapter, + F: Fn(&mut fmt::Formatter, &Record, &OwnedKVList) -> slog::Result, +{ + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + (self.fmt_fn)(f, record, values) + } + + fn priority(&self, record: &Record, values: &OwnedKVList) -> Priority { + self.inner.priority(record, values) + } +} + +impl Debug for WithFormat +where + A: Adapter, + F: Fn(&mut fmt::Formatter, &Record, &OwnedKVList) -> slog::Result, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("WithFormat") + .field("inner", &self.inner) + .finish() + } +} + +/// An [`Adapter`] that calls a closure to decide the [`Priority`] of each log +/// message. +/// +/// This is created by the [`Adapter::with_priority`] method. +/// +/// [`Adapter`]: trait.Adapter.html +/// [`Adapter::with_priority`]: trait.Adapter.html#method.with_priority +/// [`Priority`]: ../struct.Priority.html +#[derive(Clone, Copy)] +pub struct WithPriority +where + A: Adapter, + P: Fn(&Record, &OwnedKVList) -> Priority, +{ + inner: A, + priority_fn: P, +} + +impl Adapter for WithPriority +where + A: Adapter, + P: Fn(&Record, &OwnedKVList) -> Priority, +{ + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + Adapter::fmt(&self.inner, f, record, values) + } + + fn priority(&self, record: &Record, values: &OwnedKVList) -> Priority { + (self.priority_fn)(record, values) + } +} + +impl Debug for WithPriority +where + A: Adapter, + P: Fn(&Record, &OwnedKVList) -> Priority, +{ + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.debug_struct("WithPriority") + .field("inner", &self.inner) + .finish() + } +} diff --git a/src/builder.rs b/src/builder.rs index ad61fcb..8a4a737 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1,10 +1,10 @@ -use ::{Facility, get_priority}; -use format::{DefaultMsgFormat, MsgFormat}; +use ::{Facility, Priority, SyslogDrain}; +use adapter::{self, Adapter, DefaultAdapter}; use libc; -use SyslogDrain; +use slog::{self, OwnedKVList, Record}; use std::borrow::Cow; use std::ffi::{CStr, CString}; -use slog::Level; +use std::fmt; /// Builds a [`SyslogDrain`]. /// @@ -16,22 +16,20 @@ use slog::Level; /// [`SyslogDrain`]: struct.SyslogDrain.html #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] -pub struct SyslogBuilder { +pub struct SyslogBuilder { + pub(crate) adapter: A, pub(crate) facility: Facility, pub(crate) ident: Option>, pub(crate) option: libc::c_int, - pub(crate) log_priority: libc::c_int, - pub(crate) format: F, } impl Default for SyslogBuilder { fn default() -> Self { SyslogBuilder { + adapter: DefaultAdapter, facility: Facility::default(), ident: None, option: 0, - log_priority: 0, - format: DefaultMsgFormat, } } } @@ -43,7 +41,7 @@ impl SyslogBuilder { } } -impl SyslogBuilder { +impl SyslogBuilder { /// Sets the syslog facility to send logs to. /// /// By default, this is [`Facility::User`]. @@ -240,42 +238,98 @@ impl SyslogBuilder { self } - /// Log all messages with the given syslog priority - #[inline] - pub fn log_priority(mut self, log_priority: Level) -> Self { - self.log_priority = get_priority(log_priority); - self + /// Set the [`Adapter`], which handles formatting and message priorities. + /// + /// See also the [`format`] and [`priority`] methods, which offer a + /// convenient way to customize formatting or priority mapping. + /// + /// The default is [`DefaultAdapter`]. + /// + /// # Example + /// + /// ``` + /// use slog_syslog::adapter::BasicAdapter; + /// use slog_syslog::SyslogBuilder; + /// + /// let logger = SyslogBuilder::new() + /// .adapter(BasicAdapter) + /// .build(); + /// ``` + /// + /// [`Adapter`]: adapter/trait.Adapter.html + /// [`DefaultAdapter`]: adapter/struct.DefaultAdapter.html + /// [`format`]: #method.format + /// [`priority`]: #method.priority + pub fn adapter(self, adapter: A2) -> SyslogBuilder { + self.map_adapter(|_| adapter) } - - /// Set a format for log messages and structured data. + /// Modify the [`Adapter`], which handles formatting and message + /// priorities. /// - /// The default is [`DefaultMsgFormat`]. + /// This method supplies the current [`Adapter`] to the provided closure, + /// then replaces the current [`Adapter`] with the one that the closure + /// returns. + /// + /// The [`adapter`][`Self::adapter`], [`format`], and [`priority`] methods + /// all call this method to replace the [`Adapter`]. /// /// # Example /// /// ``` + /// use slog_syslog::adapter::{Adapter, DefaultAdapter}; /// use slog_syslog::SyslogBuilder; - /// use slog_syslog::format::BasicMsgFormat; /// - /// let logger = SyslogBuilder::new() - /// .format(BasicMsgFormat) + /// let drain = SyslogBuilder::new() + /// .map_adapter(|adapter| adapter.with_fmt(|f, record, _| { + /// write!(f, "here's a message: {}", record.msg())?; + /// Ok(()) + /// })) /// .build(); /// ``` /// - /// [`DefaultMsgFormat`]: format/struct.DefaultMsgFormat.html - pub fn format(self, format: F2) -> SyslogBuilder { + /// [`Self::adapter`]: #method.adapter + /// [`Adapter`]: adapter/trait.Adapter.html + /// [`format`]: #method.format + /// [`priority`]: #method.priority + pub fn map_adapter(self, f: F) -> SyslogBuilder + where + A2: Adapter, + F: FnOnce(A) -> A2, + { SyslogBuilder { + adapter: f(self.adapter), facility: self.facility, ident: self.ident, option: self.option, - log_priority: self.log_priority, - format, } } + /// Use custom message formatting. + /// + /// See [`Adapter::with_fmt`] for details and examples. + /// + /// [`Adapter::with_fmt`]: adapter/trait.Adapter.html#method.with_fmt + pub fn format(self, fmt_fn: F) -> SyslogBuilder> + where F: Fn(&mut fmt::Formatter, &Record, &OwnedKVList) -> slog::Result { + self.map_adapter(|adapter| adapter.with_fmt(fmt_fn)) + } + + /// Customize the mapping of [`slog::Level`]s to + /// [syslog priorities][`Priority`]. + /// + /// See [`Adapter::with_priority`] for details and examples. + /// + /// [`Adapter::with_priority`]: adapter/trait.Adapter.html#method.with_priority + /// [`Priority`]: struct.Priority.html + /// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html + pub fn priority(self, priority_fn: F) -> SyslogBuilder> + where F: Fn(&Record, &OwnedKVList) -> Priority { + self.map_adapter(|adapter| adapter.with_priority(priority_fn)) + } + /// Builds a `SyslogDrain` from the settings provided. - pub fn build(self) -> SyslogDrain { + pub fn build(self) -> SyslogDrain { SyslogDrain::from_builder(self) } } diff --git a/src/config.rs b/src/config.rs index 31b4190..fea537e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,14 +4,12 @@ //! [serde]: https://serde.rs/ //! [`SyslogDrain`]: ../struct.SyslogDrain.html -use Facility; -use format::{BasicMsgFormat, DefaultMsgFormat, MsgFormat}; -use slog::{self, OwnedKVList, Record, Level}; +use ::{Facility, Priority, SyslogBuilder, SyslogDrain}; +use adapter::{Adapter, BasicAdapter, DefaultAdapter}; +use slog::{self, OwnedKVList, Record}; use std::borrow::Cow; use std::ffi::CStr; use std::fmt; -use SyslogBuilder; -use SyslogDrain; #[cfg(test)] use toml; /// Deserializable configuration for a [`SyslogDrain`]. @@ -28,9 +26,9 @@ pub struct SyslogConfig { /// /// Possible values are `default` and `basic`. /// - /// See [`MsgFormat`] for more information. + /// See [`Adapter`] for more information. /// - /// [`MsgFormat`]: ../format/trait.MsgFormat.html + /// [`Adapter`]: ../adapter/trait.Adapter.html pub format: MsgFormatConfig, /// The syslog facility to send logs to. @@ -92,8 +90,13 @@ pub struct SyslogConfig { /// in garbled output. pub log_perror: bool, - /// Log all messages with the given priority - pub log_priority: Option, + /// Log some or all messages with the given [priority][`Priority`]. + /// + /// See [`Priority`] and [`Adapter::with_priority`] for more information. + /// + /// [`Adapter::with_priority`]: ../adapter/trait.Adapter.html#method.with_priority + /// [`Priority`]: ../struct.Priority.html + pub priority: PriorityConfig, #[serde(skip)] __non_exhaustive: (), @@ -106,10 +109,10 @@ impl SyslogConfig { } /// Creates a new `SyslogBuilder` from the settings. - pub fn into_builder(self) -> SyslogBuilder { + pub fn into_builder(self) -> SyslogBuilder { let b = SyslogBuilder::new() .facility(self.facility) - .format(self.format.into()); + .adapter((self.format, self.priority).into()); let b = match self.ident { Some(ident) => b.ident(ident), @@ -132,16 +135,11 @@ impl SyslogConfig { false => b, }; - let b = match self.log_priority { - Some(priority) => b.log_priority(priority), - None => b, - }; - b } /// Creates a new `SyslogDrain` from the settings. - pub fn build(self) -> SyslogDrain { + pub fn build(self) -> SyslogDrain { self.into_builder().build() } } @@ -155,20 +153,20 @@ impl Default for SyslogConfig { log_pid: false, log_delay: None, log_perror: false, - log_priority: None, + priority: PriorityConfig::default(), __non_exhaustive: (), } } } -/// Enumeration of built-in `MsgFormat`s. +/// Enumeration of built-in formatting styles. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] #[serde(rename_all = "snake_case")] pub enum MsgFormatConfig { - /// [`DefaultMsgFormat`](struct.DefaultMsgFormat.html). + /// RFC 5424-like formatting, using [`DefaultAdapter`](struct.DefaultAdapter.html). Default, - /// [`BasicMsgFormat`](struct.BasicMsgFormat.html). + /// Log the message only, not structured data, using [`BasicAdapter`](struct.BasicAdapter.html). Basic, #[doc(hidden)] @@ -181,47 +179,192 @@ impl Default for MsgFormatConfig { } } -/// Implements [`MsgFormat`] based on the settings in a [`MsgFormatConfig`]. +/// Configures mapping of [`slog::Level`]s to [syslog priorities]. +/// +/// # TOML Example +/// +/// This configuration will log [`slog::Level::Info`] messages with level +/// [`Notice`] and facility [`Daemon`], and log [`slog::Level::Critical`] +/// messages with level [`Alert`] and facility [`Mail`]: /// -/// This is the type of [`MsgFormat`] used by [`SyslogDrain`]s constructed from +/// ``` +/// # use slog_syslog::{Facility, Level, Priority}; +/// # use slog_syslog::config::{PriorityConfig, SyslogConfig}; +/// # +/// # const TOML_CONFIG: &'static str = r#" +/// ident = "foo" +/// facility = "daemon" +/// +/// [priority] +/// info = "notice" +/// critical = ["alert", "mail"] +/// # "#; +/// # +/// # let config: SyslogConfig = toml::de::from_str(TOML_CONFIG).expect("deserialization failed"); +/// # assert_eq!(config.priority, { +/// # let mut exp = PriorityConfig::new(); +/// # exp.info = Some(Priority::new(Level::Notice, None)); +/// # exp.critical = Some(Priority::new(Level::Alert, Some(Facility::Mail))); +/// # exp +/// # }); +/// ``` +/// +/// [`Alert`]: ../enum.Level.html#variant.Alert +/// [`Daemon`]: ../enum.Facility.html#variant.Daemon +/// [`Mail`]: ../enum.Facility.html#variant.Mail +/// [`Notice`]: ../enum.Level.html#variant.Notice +/// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html +/// [`slog::Level::Critical`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Critical +/// [`slog::Level::Info`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Info +/// [syslog priorities]: ../struct.Priority.html +#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] +#[serde(default)] +pub struct PriorityConfig { + /// Default priority for all messages. + /// + /// If this is not given, the [`Level`] is chosen with [`Level::from_slog`] + /// and the [`Facility`] is taken from [`SyslogConfig::facility`]. + /// + /// [`Facility`]: ../enum.Facility.html + /// [`Level`]: ../enum.Level.html + /// [`Level::from_slog`]: ../enum.Level.html#method.from_slog + /// [`SyslogConfig::facility`]: struct.SyslogConfig.html#structfield.facility + pub all: Option, + + /// Priority for [`slog::Level::Trace`]. + /// + /// [`slog::Level::Trace`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Trace + pub trace: Option, + + /// Priority for [`slog::Level::Debug`]. + /// + /// [`slog::Level::Debug`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Debug + pub debug: Option, + + /// Priority for [`slog::Level::Info`]. + /// + /// [`slog::Level::Info`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Info + pub info: Option, + + /// Priority for [`slog::Level::Warning`]. + /// + /// [`slog::Level::Warning`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Warning + pub warning: Option, + + /// Priority for [`slog::Level::Error`]. + /// + /// [`slog::Level::Error`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Error + pub error: Option, + + /// Priority for [`slog::Level::Critical`]. + /// + /// [`slog::Level::Critical`]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Critical + pub critical: Option, + + #[serde(skip)] + __non_exhaustive: (), +} + +impl PriorityConfig { + /// Creates a new `PriorityConfig` with default settings. + pub fn new() -> Self { + Default::default() + } +} + +/// Implements [`Adapter`] based on the settings in an [`MsgFormatConfig`] and +/// [`PriorityConfig`]. +/// +/// This is the type of [`Adapter`] used by [`SyslogDrain`]s constructed from /// a [`SyslogConfig`]. /// -/// [`MsgFormat`]: ../format/trait.MsgFormat.html +/// [`Adapter`]: ../adapter/trait.Adapter.html /// [`MsgFormatConfig`]: enum.MsgFormatConfig.html +/// [`PriorityConfig`]: struct.PriorityConfig.html /// [`SyslogConfig`]: struct.SyslogConfig.html /// [`SyslogDrain`]: ../struct.SyslogDrain.html #[derive(Clone, Debug)] #[cfg_attr(test, derive(PartialEq))] -pub struct ConfiguredMsgFormat { - config: MsgFormatConfig, +pub struct ConfiguredAdapter { + format: MsgFormatConfig, + priority: PriorityConfig, } -impl MsgFormat for ConfiguredMsgFormat { +impl Adapter for ConfiguredAdapter { fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - match self.config { - MsgFormatConfig::Basic => BasicMsgFormat.fmt(f, record, values), - MsgFormatConfig::Default => DefaultMsgFormat.fmt(f, record, values), + match self.format { + MsgFormatConfig::Basic => BasicAdapter.fmt(f, record, values), + MsgFormatConfig::Default => DefaultAdapter.fmt(f, record, values), MsgFormatConfig::__NonExhaustive => panic!("MsgFormatConfig::__NonExhaustive used") } } + + fn priority(&self, record: &Record, values: &OwnedKVList) -> Priority { + let priority = match record.level() { + slog::Level::Critical => self.priority.critical, + slog::Level::Error => self.priority.error, + slog::Level::Warning => self.priority.warning, + slog::Level::Debug => self.priority.debug, + slog::Level::Trace => self.priority.trace, + _ => self.priority.info, + }; + + match (priority, self.priority.all) { + (Some(priority), Some(priority_all)) => priority.overlay(priority_all), + (None, Some(priority_all)) => priority_all, + (Some(priority), None) => priority, + (None, None) => DefaultAdapter.priority(record, values), + } + } } -impl From for ConfiguredMsgFormat { +impl From for ConfiguredAdapter { fn from(config: MsgFormatConfig) -> Self { - ConfiguredMsgFormat { - config + ConfiguredAdapter { + format: config, + priority: PriorityConfig::default(), } } } +impl From for ConfiguredAdapter { + fn from(priority: PriorityConfig) -> Self { + ConfiguredAdapter { + format: MsgFormatConfig::Default, + priority, + } + } +} + +impl From<(Option, Option)> for ConfiguredAdapter { + fn from((format_opt, priority_opt): (Option, Option)) -> Self { + ConfiguredAdapter { + format: format_opt.unwrap_or(MsgFormatConfig::Default), + priority: priority_opt.unwrap_or(PriorityConfig::default()), + } + } +} + +impl From<(MsgFormatConfig, PriorityConfig)> for ConfiguredAdapter { + fn from((format, priority): (MsgFormatConfig, PriorityConfig)) -> Self { + ConfiguredAdapter { format, priority } + } +} + #[test] fn test_config() { + use Level; + const TOML_CONFIG: &'static str = r#" format = "basic" ident = "foo" facility = "daemon" log_pid = true log_perror = true + +[priority] +info = "notice" +critical = ["alert", "mail"] "#; let config: SyslogConfig = toml::de::from_str(TOML_CONFIG).expect("deserialization failed"); @@ -231,7 +374,14 @@ log_perror = true assert_eq!( builder, SyslogBuilder::new() - .format(ConfiguredMsgFormat::from(MsgFormatConfig::Basic)) + .adapter(ConfiguredAdapter::from(( + MsgFormatConfig::Basic, + PriorityConfig { + info: Some(Priority::new(Level::Notice, None)), + critical: Some(Priority::new(Level::Alert, Some(Facility::Mail))), + ..PriorityConfig::default() + } + ))) .ident_str("foo") .facility(Facility::Daemon) .log_pid() diff --git a/src/drain.rs b/src/drain.rs index 289f90e..f3219e2 100644 --- a/src/drain.rs +++ b/src/drain.rs @@ -1,7 +1,7 @@ +use adapter::{Adapter, DefaultAdapter, format}; use builder::SyslogBuilder; -use format::{DefaultMsgFormat, format, MsgFormat}; -use libc::{self, c_char, c_int}; -use slog::{self, Drain, Level, Record, OwnedKVList}; +use libc::{self, c_char}; +use slog::{self, Drain, Record, OwnedKVList}; use std::borrow::Cow; use std::cell::RefCell; use std::ffi::CStr; @@ -60,21 +60,18 @@ lazy_static! { /// /// [`Drain`]: https://docs.rs/slog/2/slog/trait.Drain.html #[derive(Debug)] -pub struct SyslogDrain { +pub struct SyslogDrain { /// The `ident` string, if it is owned by this `SyslogDrain`. /// /// This is kept so that the string can be freed (and `closelog` called, if /// necessary) when this `SyslogDrain` is dropped. unique_ident: Option>, - /// Log all messages with the given priority - log_priority: libc::c_int, - - /// The format for log messages. - format: F, + /// The adapter for formatting and prioritizing log messages. + adapter: A, } -impl SyslogDrain { +impl SyslogDrain { /// Creates a new `SyslogDrain` with all default settings. /// /// Equivalent to `SyslogBuilder::new().build()`. @@ -83,7 +80,7 @@ impl SyslogDrain { } } -impl SyslogDrain { +impl SyslogDrain { /// Creates a new `SyslogBuilder`. /// /// Equivalent to `SyslogBuilder::new()`. @@ -92,7 +89,7 @@ impl SyslogDrain { SyslogBuilder::new() } - pub(crate) fn from_builder(builder: SyslogBuilder) -> Self { + pub(crate) fn from_builder(builder: SyslogBuilder) -> Self { // `ident` is the pointer that will be passed to `openlog`, maybe null. // // `unique_ident` is the same pointer, wrapped in `Some` and `NonNull`, @@ -138,13 +135,12 @@ impl SyslogDrain { SyslogDrain { unique_ident, - log_priority: builder.log_priority, - format: builder.format, + adapter: builder.adapter, } } } -impl Drop for SyslogDrain { +impl Drop for SyslogDrain { fn drop(&mut self) { // Check if this `SyslogDrain` was created with an owned `ident` // string. @@ -219,7 +215,7 @@ impl Drop for SyslogDrain { } } -impl Drain for SyslogDrain { +impl Drain for SyslogDrain { type Ok = (); type Err = slog::Never; @@ -229,10 +225,10 @@ impl Drain for SyslogDrain { let mut tl_buf = &mut *tl_buf_mut; // Figure out the priority. - let priority = if self.log_priority > 0 { self.log_priority } else { get_priority(record.level()) }; + let priority = self.adapter.priority(record, values).into_raw(); // Format the message. - let fmt_err = format(&self.format, &mut tl_buf, record, values).err(); + let fmt_err = format(&self.adapter, &mut tl_buf, record, values).err(); // If formatting fails, use an effectively null format (which shouldn't // ever fail), and separately log the error. @@ -306,18 +302,3 @@ fn assert_format_success(_result: io::Result<()>) { #[cfg(debug)] _result.expect("unexpected formatting error"); } - - -pub(crate) fn get_priority(level: Level) -> c_int { - match level { - Level::Critical => libc::LOG_CRIT, - Level::Error => libc::LOG_ERR, - Level::Warning => libc::LOG_WARNING, - Level::Debug | Level::Trace => libc::LOG_DEBUG, - - // `slog::Level` isn't non-exhaustive, so adding any more levels - // would be a breaking change. That is highly unlikely to ever - // happen. Still, we'll handle the possibility here, just in case. - _ => libc::LOG_INFO - } -} \ No newline at end of file diff --git a/src/format.rs b/src/format.rs deleted file mode 100644 index b628367..0000000 --- a/src/format.rs +++ /dev/null @@ -1,364 +0,0 @@ -//! Ways to format syslog messages with structured data. -//! -//! See [`MsgFormat`] for more details. -//! -//! [`MsgFormat`]: trait.MsgFormat.html - -use slog::{self, KV, OwnedKVList, Record}; -use std::cell::Cell; -use std::fmt::{self, Debug, Display}; -use std::io; -use std::rc::Rc; -use std::sync::Arc; - -/// A way to format syslog messages with structured data. -/// -/// Syslog does not support structured log data. If Slog key-value pairs are to -/// be included with log messages, they must be included as part of the -/// message. Implementations of this trait determine if and how this will be -/// done. -pub trait MsgFormat: Debug { - /// Formats a log message and its key-value pairs into the given `Formatter`. - /// - /// Note that this method returns `slog::Result`, not `std::fmt::Result`. - /// The caller of this method is responsible for handling the error, - /// likely by storing it elsewhere and picking it up later. The free - /// function [`format`](fn.format.html) does just that. - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result; -} - -impl<'a, T: MsgFormat + ?Sized> MsgFormat for &'a T { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - MsgFormat::fmt(&**self, f, record, values) - } -} - -impl MsgFormat for Box { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - MsgFormat::fmt(&**self, f, record, values) - } -} - -impl MsgFormat for Rc { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - MsgFormat::fmt(&**self, f, record, values) - } -} - -impl MsgFormat for Arc { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - MsgFormat::fmt(&**self, f, record, values) - } -} - -// This helper structure provides a convenient way to implement -// `Display` with a closure. -struct ClosureAsDisplay fmt::Result>(F); -impl fmt::Result> Display for ClosureAsDisplay { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - self.0(f) - } -} - -/// Formats a log message and its key-value pairs into the given writer using -/// the given message format. -/// -/// # Errors -/// -/// This method can fail if the [`MsgFormat::fmt`] method fails, as well as if -/// the `writer` encounters an I/O error. -/// -/// [`MsgFormat::fmt`]: trait.MsgFormat.html#tymethod.fmt -pub fn format(format: F, mut writer: W, record: &Record, values: &OwnedKVList) -> slog::Result<()> { - // If there is an error calling `format.fmt`, it will be stored here. We - // have to use `Cell` because the `Display::fmt` method doesn't get a - // mutable reference to `self`. - let result: Cell> = Cell::new(None); - - // Construct our `Display` implementation… - let displayable = ClosureAsDisplay(|f| { - // Do the formatting. - if let Err(e) = MsgFormat::fmt(&format, f, record, values) { - // If there's an error, smuggle it out. - result.set(Some(e)); - } - // Pretend to succeed, even if there was an error. The real error will - // be picked up later. - Ok(()) - }); - - // …and use it to write into the given writer. - let outer_result: io::Result<()> = write!(writer, "{}", displayable); - - // If there was an I/O error, fail with that. This takes precedence over - // the `result`, because if an I/O error happened, `result` probably - // contains a `slog::Error::Fmt` that resulted from the I/O error. - if let Err(e) = outer_result { - Err(slog::Error::Io(e)) - } - // If there was a formatter/serializer error other than one caused by I/O, - // fail with that. - else if let Some(e) = result.take() { - Err(e) - } - // No error. Yay! - else { - Ok(()) - } -} - -/// An implementation of [`MsgFormat`] that discards the key-value pairs and -/// logs only the [`msg`] part of a log [`Record`]. -/// -/// [`msg`]: https://docs.rs/slog/2/slog/struct.Record.html#method.msg -/// [`MsgFormat`]: trait.MsgFormat.html -/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html -#[derive(Clone, Copy, Debug, Default)] -pub struct BasicMsgFormat; -impl MsgFormat for BasicMsgFormat { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, _: &OwnedKVList) -> slog::Result { - write!(f, "{}", record.msg()).map_err(From::from) - } -} - -/// A [`MsgFormat`] implementation that calls a closure to perform the -/// formatting. -/// -/// This is meant to provide a convenient way to implement a custom -/// `MsgFormat`. -/// -/// # Example -/// -/// ``` -/// use slog_syslog::SyslogBuilder; -/// use slog_syslog::format::CustomMsgFormat; -/// -/// let drain = SyslogBuilder::new() -/// .format(CustomMsgFormat(|f, record, _| { -/// write!(f, "here's a message: {}", record.msg())?; -/// Ok(()) -/// })) -/// .build(); -/// ``` -/// -/// Note the use of the `?` operator. The closure is expected to return -/// `Result<(), slog::Error>`, not the `Result<(), std::fmt::Error>` that -/// `write!` returns. `slog::Error` does have a conversion from -/// `std::fmt::Error`, which the `?` operator will automatically perform. -/// -/// [`MsgFormat`]: trait.MsgFormat.html -pub struct CustomMsgFormat slog::Result>(pub T); -impl slog::Result> MsgFormat for CustomMsgFormat { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - self.0(f, record, values) - } -} -impl slog::Result> Debug for CustomMsgFormat { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("CustomMsgFormat").finish() - } -} - -/// Copies input to output, but escapes characters as prescribed by RFC 5424 for PARAM-VALUEs. -struct Rfc5424LikeValueEscaper(W); - -impl fmt::Write for Rfc5424LikeValueEscaper { - fn write_str(&mut self, mut s: &str) -> fmt::Result { - while let Some(index) = s.find(|c| c == '\\' || c == '"' || c == ']') { - if index != 0 { - self.0.write_str(&s[..index])?; - } - - // All three delimiters are ASCII characters, so this won't have bogus results. - self.write_char(s.as_bytes()[index] as char)?; - - if s.len() >= index { - s = &s[(index + 1)..]; - } - else { - s = &""; - break; - } - } - - if !s.is_empty() { - self.0.write_str(s)?; - } - - Ok(()) - } - - fn write_char(&mut self, c: char) -> fmt::Result { - match c { - '\\' => self.0.write_str(r"\\"), - '"' => self.0.write_str("\\\""), - ']' => self.0.write_str("\\]"), - _ => write!(self.0, "{}", c) - } - } -} - -#[test] -fn test_rfc_5424_like_value_escaper() { - use std::iter; - - fn case(input: &str, expected_output: &str) { - let mut e = Rfc5424LikeValueEscaper(String::new()); - fmt::Write::write_str(&mut e, input).unwrap(); - assert_eq!(e.0, expected_output); - } - - // Test that each character is properly escaped. - for c in &['\\', '"', ']'] { - let ec = format!("\\{}", c); - - { - let input = format!("{}", c); - case(&*input, &*ec); - } - - for at_start_count in 0..=2 { - for at_mid_count in 0..=2 { - for at_end_count in 0..=2 { - // First, we assemble the input and expected output strings. - let mut input = String::new(); - let mut expected_output = String::new(); - - // Place the symbol(s) at the beginning of the strings. - input.extend(iter::repeat(c).take(at_start_count)); - expected_output.extend(iter::repeat(&*ec).take(at_start_count)); - - // First plain text. - input.push_str("foo"); - expected_output.push_str("foo"); - - // Middle symbol(s). - input.extend(iter::repeat(c).take(at_mid_count)); - expected_output.extend(iter::repeat(&*ec).take(at_mid_count)); - - // Second plain text. - input.push_str("bar"); - expected_output.push_str("bar"); - - // End symbol(s). - input.extend(iter::repeat(c).take(at_end_count)); - expected_output.extend(iter::repeat(&*ec).take(at_end_count)); - - // Finally, test this combination. - case(&*input, &*expected_output); - }}} - } - - case("", ""); - case("foo", "foo"); - case("[foo]", "[foo\\]"); - case("\\\"]", "\\\\\\\"\\]"); // \"] ⇒ \\\"\] -} - -/// An implementation of [`MsgFormat`] that formats the key-value pairs of a -/// log [`Record`] similarly to [RFC 5424]. -/// -/// # Not really RFC 5424 -/// -/// This does not actually generate conformant RFC 5424 STRUCTURED-DATA. The -/// differences are: -/// -/// * All key-value pairs are placed into a single SD-ELEMENT. -/// * The SD-ELEMENT does not contain an SD-ID, only SD-PARAMs. -/// * PARAM-NAMEs are encoded in UTF-8, not ASCII. -/// * Forbidden characters in PARAM-NAMEs are not filtered out, nor is an error -/// raised if a key contains such characters. -/// -/// # Example output -/// -/// Given a log message `Hello, world!`, where the key `key1` has the value -/// `value1` and `key2` has the value `value2`, the formatted message will be -/// `Hello, world! [key1="value1" key2="value2"]` (possibly with `key2` first -/// instead of `key1`). -/// -/// [`MsgFormat`]: trait.MsgFormat.html -/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html -/// [RFC 5424]: https://tools.ietf.org/html/rfc5424 -#[derive(Clone, Copy, Debug, Default)] -pub struct DefaultMsgFormat; -impl MsgFormat for DefaultMsgFormat { - fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { - struct SerializerImpl<'a, 'b: 'a> { - f: &'a mut fmt::Formatter<'b>, - is_first_kv: bool, - } - - impl<'a, 'b> SerializerImpl<'a, 'b> { - fn new(f: &'a mut fmt::Formatter<'b>) -> Self { - Self { f, is_first_kv: true } - } - - fn finish(&mut self) -> slog::Result { - if !self.is_first_kv { - write!(self.f, "]")?; - } - Ok(()) - } - } - - impl<'a, 'b> slog::Serializer for SerializerImpl<'a, 'b> { - fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { - use std::fmt::Write; - - self.f.write_str(if self.is_first_kv {" ["} else {" "})?; - self.is_first_kv = false; - - // Write the key unaltered, but escape the value. - // - // RFC 5424 does not allow space, ']', '"', or '\' to - // appear in PARAM-NAMEs, and does not allow such - // characters to be escaped. - write!(self.f, "{}=\"", key)?; - write!(Rfc5424LikeValueEscaper(&mut self.f), "{}", val)?; - self.f.write_char('"')?; - Ok(()) - } - } - - write!(f, "{}", record.msg())?; - - { - let mut serializer = SerializerImpl::new(f); - - values.serialize(record, &mut serializer)?; - record.kv().serialize(record, &mut serializer)?; - serializer.finish()?; - } - - Ok(()) - } -} - -/// Makes sure the example output for `DefaultMsgFormat` is what it actually -/// generates. -#[test] -fn test_default_msg_format() { - use slog::Level; - - let mut buf = Vec::new(); - - format( - DefaultMsgFormat, - &mut buf, - &record!( - Level::Info, - "", - &format_args!("Hello, world!"), - b!("key1" => "value1") - ), - &o!("key2" => "value2").into(), - ).expect("formatting failed"); - - let result = String::from_utf8(buf).expect("invalid UTF-8"); - - assert!( - // The KVs' order is not well-defined, so they might get reversed. - result == "Hello, world! [key1=\"value1\" key2=\"value2\"]" || - result == "Hello, world! [key2=\"value2\" key1=\"value1\"]" - ); -} diff --git a/src/level.rs b/src/level.rs new file mode 100644 index 0000000..5e7362f --- /dev/null +++ b/src/level.rs @@ -0,0 +1,207 @@ +use libc::{self, c_int}; +use slog; +use std::error::Error; +use std::fmt::{self, Display, Formatter}; +use std::str::FromStr; + +/// A syslog severity level. Conversions are provided to and from `c_int`. Not +/// to be confused with [`slog::Level`]. +/// +/// Available levels are platform-independent. They were originally defined by +/// BSD, are specified by POSIX, and this author is not aware of any system +/// that has a different set of log severities. +/// +/// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html +#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[cfg_attr(feature = "serde", derive(Deserialize, Serialize))] +#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))] +pub enum Level { + /// Verbose debugging messages. + Debug, + + /// Normal informational messages. A program that's starting up might log + /// its version number at this level. + Info, + + /// The situation is not an error, but it probably needs attention. + Notice, + + /// Warning. Something has probably gone wrong. + #[cfg_attr(feature = "serde", serde(alias = "warn"))] + Warning, + + /// Error. Something has definitely gone wrong. + #[cfg_attr(feature = "serde", serde(alias = "error"))] + Err, + + /// Critical error. Hardware failures fall under this level. + Crit, + + /// Something has happened that requires immediate action. + Alert, + + /// The system has failed. This level is for kernel panics and similar + /// system-wide failures. + #[cfg_attr(feature = "serde", serde(alias = "panic"))] + Emerg, +} + +impl Level { + /// Gets the name of this `Level`, like `emerg` or `notice`. + /// + /// The `FromStr` implementation accepts the same names, but it is + /// case-insensitive. + pub fn name(&self) -> &'static str { + match *self { + Level::Emerg => "emerg", + Level::Alert => "alert", + Level::Crit => "crit", + Level::Err => "err", + Level::Warning => "warning", + Level::Notice => "notice", + Level::Info => "info", + Level::Debug => "debug", + } + } + + /// Converts a `libc::LOG_*` numeric constant to a `Level` value. + /// + /// Returns `Some` if the value is a valid level, or `None` if not. + pub fn from_int(value: c_int) -> Option { + match value { + libc::LOG_EMERG => Some(Level::Emerg), + libc::LOG_ALERT => Some(Level::Alert), + libc::LOG_CRIT => Some(Level::Crit), + libc::LOG_ERR => Some(Level::Err), + libc::LOG_WARNING => Some(Level::Warning), + libc::LOG_NOTICE => Some(Level::Notice), + libc::LOG_INFO => Some(Level::Info), + libc::LOG_DEBUG => Some(Level::Debug), + _ => None, + } + } + + /// Maps a [`slog::Level`] to a syslog level. + /// + /// Mappings are as follows: + /// + /// * [`Critical`][slog critical] ⇒ [`Crit`][syslog crit] + /// * [`Error`][slog error] ⇒ [`Err`][syslog err] + /// * [`Warning`][slog warning] ⇒ [`Warning`][syslog warning] + /// * [`Info`][slog info] ⇒ [`Info`][syslog info] + /// * [`Debug`][slog debug] ⇒ [`Debug`][syslog debug] + /// * [`Trace`][slog trace] ⇒ [`Debug`][syslog debug] + /// + /// This is used by the default implementation of [`Adapter::priority`]. + /// + /// [`Adapter::priority`]: adapter/trait.Adapter.html#method.priority + /// [`Priority`]: struct.Priority.html + /// [`slog::Level`]: https://docs.rs/slog/2/slog/enum.Level.html + /// [slog critical]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Critical + /// [slog error]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Error + /// [slog warning]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Warning + /// [slog info]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Info + /// [slog debug]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Debug + /// [slog trace]: https://docs.rs/slog/2/slog/enum.Level.html#variant.Trace + /// [syslog crit]: #variant.Crit + /// [syslog err]: #variant.Err + /// [syslog warning]: #variant.Warning + /// [syslog info]: #variant.Info + /// [syslog debug]: #variant.Debug + pub fn from_slog(level: slog::Level) -> Self { + match level { + slog::Level::Critical => Level::Crit.into(), + slog::Level::Error => Level::Err.into(), + slog::Level::Warning => Level::Warning.into(), + slog::Level::Debug | slog::Level::Trace => Level::Debug.into(), + + // `slog::Level` isn't non-exhaustive, so adding any more levels + // would be a breaking change. That is highly unlikely to ever + // happen. Still, we'll handle the possibility here, just in case. + _ => Level::Info.into() + } + } +} + +impl Display for Level { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl From for c_int { + fn from(level: Level) -> Self { + match level { + Level::Emerg => libc::LOG_EMERG, + Level::Alert => libc::LOG_ALERT, + Level::Crit => libc::LOG_CRIT, + Level::Err => libc::LOG_ERR, + Level::Warning => libc::LOG_WARNING, + Level::Notice => libc::LOG_NOTICE, + Level::Info => libc::LOG_INFO, + Level::Debug => libc::LOG_DEBUG, + } + } +} + +impl FromStr for Level { + type Err = UnknownLevelError; + + fn from_str(s: &str) -> Result::Err> { + let s = s.to_ascii_lowercase(); + + match &*s { + "emerg" | "panic" => Ok(Level::Emerg), + "alert" => Ok(Level::Alert), + "crit" => Ok(Level::Crit), + "err" | "error" => Ok(Level::Err), + "warning" | "warn" => Ok(Level::Warning), + "notice" => Ok(Level::Notice), + "info" => Ok(Level::Info), + "debug" => Ok(Level::Debug), + _ => Err(UnknownLevelError { + name: s, + }) + } + } +} + +/// Indicates that `::from_str` was called with an unknown +/// level name. +#[derive(Clone, Debug)] +#[cfg_attr(test, derive(Eq, PartialEq))] +pub struct UnknownLevelError { + name: String, +} + +impl UnknownLevelError { + /// The unrecognized level name. + pub fn name(&self) -> &str { + &*self.name + } +} + +impl Display for UnknownLevelError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "unrecognized syslog level name `{}`", self.name) + } +} + +impl Error for UnknownLevelError { + #[allow(deprecated)] // Old versions of Rust require this. + fn description(&self) -> &str { + "unrecognized syslog level name" + } +} + +#[test] +fn test_level_from_str() { + assert_eq!(Level::from_str("notice"), Ok(Level::Notice)); + assert_eq!(Level::from_str("foobar"), Err(UnknownLevelError { name: "foobar".to_string() })); + assert_eq!(Level::from_str("foobar").unwrap_err().to_string(), "unrecognized syslog level name `foobar`"); +} + +#[test] +fn test_level_ordering() { + assert!(Level::Debug < Level::Emerg); +} diff --git a/src/lib.rs b/src/lib.rs index 0144021..66c5fb6 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,7 +35,7 @@ //! //! POSIX doesn't support opening more than one connection to the syslog server //! at a time. All [`SyslogBuilder` settings] except -//! [`format`][`SyslogBuilder::format`] are stored in global variables by the +//! [`adapter`][`SyslogBuilder::adapter`] are stored in global variables by the //! platform libc, are overwritten whenever the POSIX `openlog` function is //! called (which happens when a [`SyslogDrain`] is created), and are reset //! whenever the POSIX `closelog` function is called (which may happen when a @@ -55,11 +55,11 @@ //! //! Failure to abide by these rules may result in `closelog` being called at //! the wrong time. This will cause [`SyslogBuilder` settings] (except -//! [`format`][`SyslogBuilder::format`]) to be reset, and there may be a delay +//! [`adapter`][`SyslogBuilder::adapter`]) to be reset, and there may be a delay //! in processing the next log message after that (because the connection to //! the syslog server, if applicable, must be reopened). //! -//! [`SyslogBuilder::format`]: struct.SyslogBuilder.html#method.format +//! [`SyslogBuilder::adapter`]: struct.SyslogBuilder.html#method.adapter //! [`SyslogBuilder` settings]: struct.SyslogBuilder.html#impl //! [`SyslogDrain`]: struct.SyslogDrain.html @@ -141,6 +141,8 @@ extern crate slog; #[cfg(all(test, feature = "serde"))] extern crate toml; +pub mod adapter; + mod builder; pub use builder::*; @@ -158,4 +160,8 @@ mod mock; #[cfg(test)] mod tests; -pub mod format; +mod level; +pub use level::*; + +mod priority; +pub use priority::*; diff --git a/src/priority.rs b/src/priority.rs new file mode 100644 index 0000000..c792071 --- /dev/null +++ b/src/priority.rs @@ -0,0 +1,215 @@ +use Facility; +use Level; +use libc::c_int; +use std::cmp::{Eq, PartialEq}; +use std::hash::{Hash, Hasher}; + +#[cfg(feature = "serde")] +use serde::{Deserialize, ser, Serialize, Serializer}; + +/// A syslog priority (combination of [severity level] and [facility]). +/// +/// Each message sent to syslog has a “priority”, which consists of a +/// required [severity level] and an optional [facility]. This structure +/// represents a priority, either as symbolic level and facility (created with +/// the [`new`] method), or as a raw numeric value (created with the +/// [`from_raw`] method). +/// +/// To customize the syslog priorities of log messages, implement +/// [`Adapter::priority`]. The easiest way to do that is to call +/// [`Adapter::with_priority`] on an existing [`Adapter`], such as +/// [`DefaultAdapter`]. +/// +/// Several convenient `From` implementations are also provided. `From` +/// is not provided because it would be unsound (see the “safety” section of +/// the documentation for the [`from_raw`] method). +/// +/// # Examples +/// +/// See the documentation for [`Adapter::with_priority`] for example usage. +/// +/// [`Adapter`]: adapter/trait.Adapter.html +/// [`Adapter::priority`]: adapter/trait.Adapter.html#method.priority +/// [`Adapter::with_priority`]: adapter/trait.Adapter.html#method.with_priority +/// [`DefaultAdapter`]: adapter/struct.DefaultAdapter.html +/// [facility]: enum.Facility.html +/// [`from_raw`]: #method.from_raw +/// [`new`]: #method.new +/// [severity level]: enum.Level.html +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serde", derive(Deserialize))] +#[cfg_attr(feature = "serde", serde(from = "PrioritySerde"))] +pub struct Priority(PriorityKind); + +impl Priority { + /// Creates a new `Priority` consisting of the given `Level` and + /// `Option`. + pub fn new(level: Level, facility: Option) -> Self { + Priority(PriorityKind::Normal(level, facility)) + } + + /// The `Level` that this `Priority` was created with. + /// + /// This will be `None` if this `Priority` was created with the + /// [`from_raw`] method. + /// + /// [`from_raw`]: #method.from_raw + pub fn level(self) -> Option { + match self.0 { + PriorityKind::Normal(level, _) => Some(level), + PriorityKind::Raw(_) => None, + } + } + + /// The `Facility` that this `Priority` was created with, if any. + /// + /// This will be `None` if this `Priority` was created without a `Facility` + /// or if this `Priority` was created with the [`from_raw`] method. + /// + /// [`from_raw`]: #method.from_raw + pub fn facility(self) -> Option { + match self.0 { + PriorityKind::Normal(_, facility) => facility, + PriorityKind::Raw(_) => None, + } + } + + /// Fills in the facility from another `Priority`. + /// + /// A `Priority` can contain a [`Level`], a [`Level`] and [`Facility`], or + /// a raw numeric value. If this `Priority` contains only a [`Level`], then + /// this method will take the [`Facility`] from the other `Priority`, + /// creating a new, combined `Priority`. + /// + /// This method simply returns `self` if `other` doesn't have a facility + /// either, or if `other` was created using [`from_raw`]. + /// + /// # Example + /// + /// ``` + /// use slog_syslog::{Facility, Level, Priority}; + /// + /// let defaults = Priority::new(Level::Notice, Some(Facility::Mail)); + /// let priority = Priority::new(Level::Err, None); + /// let overlaid = priority.overlay(defaults); + /// + /// assert_eq!(overlaid, Priority::new(Level::Err, Some(Facility::Mail))); + /// ``` + /// + /// [`Facility`]: enum.Facility.html + /// [`from_raw`]: #method.from_raw + /// [`Level`]: enum.Level.html + pub fn overlay(self, other: Priority) -> Priority { + match (self.0, other.0) { + ( + PriorityKind::Normal(level, None), + PriorityKind::Normal(_, Some(facility)), + ) => Priority::new(level, Some(facility)), + _ => self, + } + } + + /// Creates a new `Priority` from the given raw numeric value. + /// + /// # Safety + /// + /// The numeric priority value must be valid for the system that the + /// program is running on, using the `libc::LOG_*` constants. [POSIX] does + /// not specify what happens if an incorrect numeric priority value is + /// passed to the system `syslog` function. + /// + /// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html + pub unsafe fn from_raw(priority: c_int) -> Self { + Priority(PriorityKind::Raw(priority)) + } + + /// Converts this `Priority` into a raw numeric value, as accepted by the + /// system `syslog` function. + pub fn into_raw(self) -> c_int { + match self.0 { + PriorityKind::Normal(level, facility) => + c_int::from(level) | facility.map(c_int::from).unwrap_or(0), + + PriorityKind::Raw(priority) => priority, + } + } +} + +impl PartialEq for Priority { + fn eq(&self, other: &Priority) -> bool { + self.into_raw() == other.into_raw() + } +} + +impl Eq for Priority {} + +impl Hash for Priority { + fn hash(&self, state: &mut H) { + self.into_raw().hash(state) + } +} + +impl From for Priority { + fn from(level: Level) -> Self { + Priority::new(level, None) + } +} + +impl From<(Level, Option)> for Priority { + fn from((level, facility): (Level, Option)) -> Self { + Priority::new(level, facility) + } +} + +impl From<(Level, Facility)> for Priority { + fn from((level, facility): (Level, Facility)) -> Self { + Priority::new(level, Some(facility)) + } +} + +#[derive(Clone, Copy, Debug)] +enum PriorityKind { + Normal(Level, Option), + Raw(c_int), +} + +#[test] +fn test_into_raw() { + use libc; + + let prio = Priority::new(Level::Warning, Some(Facility::Local3)); + assert_eq!(prio.into_raw(), libc::LOG_WARNING | libc::LOG_LOCAL3); + + let prio = Priority::new(Level::Alert, None); + assert_eq!(prio.into_raw(), libc::LOG_ALERT); +} + +#[cfg(feature = "serde")] +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(untagged)] +enum PrioritySerde { + LevelOnly(Level), + LevelAndFacility(Level, Facility), +} + +#[cfg(feature = "serde")] +impl From for Priority { + fn from(priority: PrioritySerde) -> Priority { + match priority { + PrioritySerde::LevelOnly(level) => Priority::new(level, None), + PrioritySerde::LevelAndFacility(level, facility) => Priority::new(level, Some(facility)), + } + } +} + +#[cfg(feature = "serde")] +impl Serialize for Priority { + fn serialize(&self, serializer: S) -> Result + where S: Serializer { + match (self.level(), self.facility()) { + (None, _) => return Err(ser::Error::custom("cannot serialize a `Priority` that was created with `Priority::from_raw`")), + (Some(level), None) => PrioritySerde::LevelOnly(level), + (Some(level), Some(facility)) => PrioritySerde::LevelAndFacility(level, facility), + }.serialize(serializer) + } +} diff --git a/src/tests.rs b/src/tests.rs index 1f44cba..5be2262 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,11 +1,8 @@ -use Facility; -use format::CustomMsgFormat; +use ::{Facility, Level, mock, SyslogBuilder}; use libc; -use mock; -use slog::{self, Logger, Level}; +use slog::{self, Logger}; use std::borrow::Cow; use std::ffi::CStr; -use SyslogBuilder; #[test] fn test_log() { @@ -39,7 +36,7 @@ fn test_log() { let logger2 = Logger::root_typed(SyslogBuilder::new() .facility(Facility::Local1) .ident(Cow::Borrowed(CStr::from_bytes_with_nul(b"logger2\0").unwrap())) - .format(CustomMsgFormat(|_, _, _| Err(slog::Error::Other))) + .format(|_, _, _| Err(slog::Error::Other)) .build(), o!()); info!(logger2, "Message from second logger while first still active."; "key" => "value"); @@ -49,15 +46,13 @@ fn test_log() { _ => false, }); - let logger3 = Logger::root_typed(SyslogBuilder::new() .facility(Facility::Local1) - .log_priority(Level::Error) .ident(Cow::Borrowed(CStr::from_bytes_with_nul(b"logger3\0").unwrap())) - .format(CustomMsgFormat(|_, _, _| Err(slog::Error::Other))) + .priority(|_, _| (Level::Alert, Facility::Local2).into()) .build(), o!()); - info!(logger3, "Message from third logger while first still active."; "key" => "value"); + info!(logger3, "Message from third logger. Should have overridden priority."); mock::wait_for_event_matching(|event| match event { mock::Event::SysLog { message, .. } => message == &slog::Error::Other.to_string(), @@ -105,24 +100,18 @@ fn test_log() { message_f: "Error fully formatting the previous log message: %s".to_string(), message: slog::Error::Other.to_string(), }, - mock::Event::OpenLog { facility: libc::LOG_LOCAL1, flags: 0, ident: "logger3".to_string(), }, mock::Event::SysLog { - priority: libc::LOG_ERR, + priority: libc::LOG_ALERT | libc::LOG_LOCAL2, message_f: "%s".to_string(), - message: "Message from third logger while first still active.".to_string(), - }, - mock::Event::SysLog { - priority: libc::LOG_ERR, - message_f: "Error fully formatting the previous log message: %s".to_string(), - message: slog::Error::Other.to_string(), + message: "Message from third logger. Should have overridden priority.".to_string(), }, mock::Event::DropOwnedIdent("example-app".to_string()), - // No `CloseLog` for `logger2` and `logger3` because it doesn't own its `ident`. + // No `CloseLog` for `logger2` and `logger3` because they don't own their `ident`. ]; assert!(events == expected_events, "events didn't match\ngot: {:#?}\nexpected: {:#?}", events, expected_events);