diff --git a/Cargo.toml b/Cargo.toml index 620ea5f..a556865 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "slog-syslog" -version = "0.12.0" +version = "0.13.0" authors = ["Dawid Ciężarkiewicz ", "William Laeder "8dfljdf"); //! -//! // log to a local unix sock `/var/run/syslog` +//! // log to the local syslog daemon //! 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) +//! Err(e) => eprintln!("Failed to start syslog. Error {:?}", e) //! }; //! } //! ``` #![warn(missing_docs)] -extern crate nix; +extern crate hostname; +#[cfg_attr(test, macro_use)] // Slog macros are only used in tests. 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::{env, fmt, process}; +use std::cell::{Cell, RefCell}; +use std::error::Error as StdError; +use std::io::Write; use std::net::SocketAddr; -use std::io::{Error, ErrorKind}; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; + +#[cfg(test)] +use std::iter; + +#[cfg(not(unix))] +use std::net::Ipv4Addr; use slog::KV; pub use syslog::Facility; -thread_local! { - static TL_BUF: RefCell> = RefCell::new(Vec::with_capacity(128)) +/// Implements `Display` with a closure. +struct ClosureAsDisplay) -> fmt::Result>(F); +impl) -> fmt::Result> fmt::Display for ClosureAsDisplay { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0(f) + } } -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`] that writes log records to a [`syslog::Logger`]. +/// +/// [`SyslogBuilder`] provides a convenient API for constructing this. +/// +/// This drain is not thread-safe (that is, it does not implement [`Sync`]). It cannot be directly used as the `Drain` underlying a [`slog::Logger`]. It must be wrapped in a [`Mutex`], a [`slog_async::Async`] (from the [slog-async] crate), or some other synchronization mechanism. +/// +/// [`Drain`]: https://docs.rs/slog/2/slog/trait.Drain.html +/// [`Mutex`]: https://doc.rust-lang.org/std/sync/struct.Mutex.html +/// [slog-async]: https://docs.rs/slog-async/2/slog_async/index.html +/// [`slog_async::Async`]: https://docs.rs/slog-async/2/slog_async/struct.Async.html +/// [`slog::Logger`]: https://docs.rs/slog/2/slog/struct.Logger.html +/// [`Sync`]: https://doc.rust-lang.org/std/marker/trait.Sync.html +/// [`SyslogBuilder`]: struct.SyslogBuilder.html +/// [`syslog::Logger`]: https://docs.rs/syslog/5/syslog/struct.Logger.html +pub struct Streamer3164 { + io: RefCell>, + + msg_format: F, + + level_filter: Option, } -/// 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 { + /// Creates a new `Streamer3164` using the given [`syslog::Logger`]. + /// + /// The new `Streamer3164` uses [`BasicMsgFormat3164`] for formatting log messages and associated key-value pairs. To use a different format, give it to the [`with_msg_format`] method. + /// + /// [`BasicMsgFormat3164`]: struct.BasicMsgFormat3164.html + /// [`syslog::Logger`]: https://docs.rs/syslog/5/syslog/struct.Logger.html + /// [`with_msg_format`]: #method.with_msg_format + pub fn new(logger: syslog::Logger) -> Self { + Streamer3164 { + io: RefCell::new(logger), + msg_format: BasicMsgFormat3164, + level_filter: None, + } + } } -impl Streamer3164 { - /// Create new syslog ``Streamer` using given `format` - pub fn new(logger: Box) -> Self { +impl Streamer3164 { + /// Replaces this `Streamer3164` with one that uses the given [`MsgFormat3164`]. + /// + /// [`MsgFormat3164`]: trait.MsgFormat3164.html + pub fn with_msg_format(self, msg_format: F2) -> Streamer3164 { Streamer3164 { - io: Mutex::new(logger), - format: Format3164::new(), + io: self.io, + msg_format, + level_filter: self.level_filter, } } + + /// Borrows the [`MsgFormat3164`] being used by this `Streamer3164`. + /// + /// [`MsgFormat3164`]: trait.MsgFormat3164.html + pub fn msg_format(&self) -> &F { + &self.msg_format + } } -impl Drain for Streamer3164 { - type Err = io::Error; +impl Drain for Streamer3164 { + type Err = syslog::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; - } + fn log(&self, info: &Record, values: &OwnedKVList) -> Result { + if let Some(level_filter) = self.level_filter { + if !info.level().is_at_least(level_filter) { + return Ok(()); + } + } + + let mut guard = self.io.borrow_mut(); + let logger = &mut *guard; + + // We need to smuggle errors out of the `Display` implementation for `msg`, below. The `Display::fmt` method takes `&self` rather than `&mut self`, so the obvious way of accomplishing this (`let mut msg_format_result: syslog::Result<()>`) will not work because the closure cannot write to any captured variable (including `msg_format_result`). + // + // Instead, we use a `Cell` to store the error. This is a somewhat unconventional use for `Cell` (which usually appears as a `struct` member, not a local variable like this), but it fits: it creates a mutable place in memory for us to store the error. + let msg_format_result = Cell::::new(Ok(())); + + // `syslog::Formatter3164` accepts any `T: Display` as a log message. The easy way to work with this would be to just allocate a new `String`, write both the KVs and the message into it, then submit that to the `Formatter3164`. + // + // But that would involve at least one and probably several unnecessary allocations. Instead, we'll give it this custom `Display` implementation, which writes the KVs and log message piece-by-piece to the output. + // + // This still isn't zero-copy, as the `core::fmt` module still allocates a buffer to write the entire syslog packet into, but one copy is still faster than two copies. + let msg = ClosureAsDisplay(|f| { + self.msg_format.fmt(f, info, values) + .map_err(|error| { + // `Cell::replace` returns the old value, which in this case is a `Result`. We're dropping it, but this triggers a warning by default, so we use `let _ =` to inform Rust that yes, we really do mean to drop it. + let _ = msg_format_result.replace(Err(/*match*/ error /*{ + // `std::fmt::Error` contains no information at all, so just pass it through. + slog::Error::Fmt(fmt::Error) => return fmt::Error, + + // Any other error needs to be preserved. + slog::Error::Io(error) => syslog::Error::from_kind(syslog::ErrorKind::Io(error)), + error => { + let kind = syslog::ErrorKind::Msg(error.to_string()); + syslog::Error::with_chain(error, kind) } + }*/)); + fmt::Error + }) + }); + + // Submit the message to syslog. + let log_result = match info.level() { + Level::Critical => logger.crit(msg), + Level::Error => logger.err(msg), + Level::Warning => logger.warning(msg), + Level::Info => logger.info(msg), + Level::Debug | Level::Trace => logger.debug(msg), + }; + + // Extract the result stored in `msg_format_result`. + // + // We'd use `Cell::take` instead, but `Result<(), _>` does not implement `Default`, so we use this instead. `Cell::replace` returns whatever's currently stored in the cell, same as `take`, so that works too. + // + // `Result` arguably should implement `Default` for any `T: Default`, but it doesn't, so whatever. + let msg_format_result = msg_format_result.replace(Ok(())); - Ok(()) + // Now, we have two results: one from `logger`, and one from `msg`. Figure out which to return. + { + // A significant error is one that isn't just `std::fmt::Error`, which contains no information. + fn has_significant_error(result: &syslog::Result<()>) -> bool { + if let Err(error) = result { + if let Some(source) = error.source() { + if !source.is::() { + return true; + } + } } - }(); - buf.clear(); - res - }) + + false + } + + // If `log_result` contains a significant error, then return it. + if has_significant_error(&log_result) { + log_result + } + // If `msg_format_result` contains an error, translate it into a `syslog::Error` and return that. + else if let Err(msg_format_err) = msg_format_result { + Err(match msg_format_err { + error @ slog::Error::Fmt(fmt::Error) => syslog::Error::with_chain(error, syslog::ErrorKind::Format), + slog::Error::Io(error) => syslog::Error::from_kind(syslog::ErrorKind::Io(error)), + error => { + let kind = syslog::ErrorKind::Msg(error.to_string()); + syslog::Error::with_chain(error, kind) + } + }) + } + // Otherwise, just return the `log_result`. + else { + log_result + } + } } } -/// Formatter to format defined in RFC 3164 -pub struct Format3164; +impl From> for Streamer3164 { + fn from(logger: syslog::Logger) -> Self { + Self::new(logger) + } +} -impl Format3164 { - /// Create new `Format3164` - pub fn new() -> Self { - Format3164 +/// A way to format log messages with structured data for the BSD syslog protocol. +/// +/// The BSD syslog protocol, as described by [RFC 3164], 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. +/// +/// [RFC 3164]: https://tools.ietf.org/html/rfc3164 +pub trait MsgFormat3164 { + /// Formats a log message and its key-value pairs into the given `Formatter`. + /// + /// This method is only formatting the `CONTENT` part of a syslog message. + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result; +} + +impl MsgFormat3164 for &T { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + (**self).fmt(f, record, values) } +} - fn format( - &self, - io: &mut io::Write, - record: &Record, - logger_kv: &OwnedKVList, - ) -> io::Result<()> { - try!(write!(io, "{}", record.msg())); +impl MsgFormat3164 for Box { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + (**self).fmt(f, record, values) + } +} - let mut ser = KSV::new(io); - { - try!(logger_kv.serialize(record, &mut ser)); - try!(record.kv().serialize(record, &mut ser)); +impl MsgFormat3164 for Rc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + (**self).fmt(f, record, values) + } +} + +impl MsgFormat3164 for Arc { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + (**self).fmt(f, record, values) + } +} + +/// An implementation of [`MsgFormat3164`] 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 +/// [`MsgFormat3164`]: trait.MsgFormat3164.html +/// [`Record`]: https://docs.rs/slog/2/slog/struct.Record.html +#[derive(Clone, Copy, Debug, Default)] +pub struct NullMsgFormat3164; +impl MsgFormat3164 for NullMsgFormat3164 { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, _: &OwnedKVList) -> slog::Result { + write!(f, "{}", record.msg())?; + Ok(()) + } +} + +/// 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) + } + } } -/// Key-Separator-Value serializer -struct KSV { - io: W, +#[test] +fn test_rfc_5424_like_value_escaper() { + 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("\\\"]", "\\\\\\\"\\]"); // \"] ⇒ \\\"\] } -impl KSV { - fn new(io: W) -> Self { - KSV { io: io } +/// An implementation of [`MsgFormat3164`] 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`). +/// +/// [`MsgFormat3164`]: trait.MsgFormat3164.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 BasicMsgFormat3164; +impl MsgFormat3164 for BasicMsgFormat3164 { + fn fmt(&self, f: &mut fmt::Formatter, record: &Record, values: &OwnedKVList) -> slog::Result { + struct Basic3164Serializer<'a, 'b> { + f: &'a mut fmt::Formatter<'b>, + is_first_kv: bool, + } + + impl<'a, 'b> Basic3164Serializer<'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 Basic3164Serializer<'a, 'b> { + fn emit_arguments(&mut self, key: slog::Key, val: &fmt::Arguments) -> slog::Result { + use 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 = Basic3164Serializer::new(f); + + values.serialize(record, &mut serializer)?; + record.kv().serialize(record, &mut serializer)?; + serializer.finish()?; + } + + Ok(()) } } -impl slog::Serializer for KSV { - fn emit_arguments(&mut self, key: &str, val: &fmt::Arguments) -> slog::Result { - try!(write!(self.io, ", {}: {}", key, val)); +/// Makes sure the example output for `BasicMsgFormat3164` is what it actually generates. +#[test] +fn test_basic_msg_format_3164() { + let result = ClosureAsDisplay(|f| { + BasicMsgFormat3164.fmt( + f, + &record!( + Level::Info, + "", + &format_args!("Hello, world!"), + b!("key1" => "value1", "key2" => "value2") + ), + &o!().into() + ).unwrap(); Ok(()) + }).to_string(); + + 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\"]" + ); +} + +#[doc(hidden)] +#[deprecated(since = "0.13.0", note = "no longer used")] +pub struct Format3164; + +#[allow(deprecated)] +#[doc(hidden)] +impl Format3164 { + pub fn new() -> Self { + Format3164 } } +#[derive(Clone, Debug)] enum SyslogKind { + UnixDefault, Unix { path: PathBuf, }, Tcp { server: SocketAddr, - hostname: String, }, Udp { local: SocketAddr, host: SocketAddr, - hostname: String, }, } +impl Default for SyslogKind { + #[cfg(unix)] + fn default() -> Self { + SyslogKind::UnixDefault + } -/// Builder pattern for constructing a syslog -pub struct SyslogBuilder { + #[cfg(not(unix))] + fn default() -> Self { + SyslogKind::Udp { + local: SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 0), + host: SocketAddr::new(Ipv4Addr::new(127, 0, 0, 1).into(), 514) + } + } +} + +/// Builder pattern for constructing a syslog drain that uses RFC 3164 (BSD) style. +/// +/// All settings have default values. `SyslogBuilder::new().start()` will give you a sensibly configured log drain, but you might especially want to customize the `facility`. +/// +/// Default settings are: +/// +/// * Facility: `LOG_USER` +/// * Level: all +/// * Transport (Unix-like platforms): Unix socket `/dev/log` or `/var/run/log` +/// * Transport (other platforms): UDP to 127.0.0.1:514 +/// * Message format: [`BasicMsgFormat3164`] +/// * Process name: the file name portion of [`std::env::current_exe()`] +/// * PID: [`std::process::id()`] +/// * Hostname: [`hostname::get()`] +/// +/// [`BasicMsgFormat3164`]: struct.BasicMsgFormat3164.html +/// [`std::env::current_exe()`]: https://doc.rust-lang.org/std/env/fn.current_exe.html +/// [`std::process::id()`]: https://doc.rust-lang.org/std/process/fn.id.html +/// [`hostname::get()`]: https://docs.rs/hostname/0.3.0/hostname/fn.get.html +#[derive(Clone, Debug)] +pub struct SyslogBuilder { facility: Option, - level: syslog::Severity, - logkind: Option, + hostname: Option, + level: Option, + logkind: SyslogKind, + msg_format: F, + pid: Option, + process: Option, } impl Default for SyslogBuilder { fn default() -> Self { - Self { + SyslogBuilder { facility: None, - level: syslog::Severity::LOG_DEBUG, - logkind: None, + hostname: None, + level: None, + logkind: SyslogKind::default(), + msg_format: BasicMsgFormat3164, + pid: None, + process: None, } } } @@ -197,8 +549,13 @@ impl SyslogBuilder { pub fn new() -> SyslogBuilder { Self::default() } - +} +impl SyslogBuilder { /// Set syslog Facility + /// + /// The default facility, as per [POSIX], is `LOG_USER`. + /// + /// [POSIX]: https://pubs.opengroup.org/onlinepubs/9699919799/functions/closelog.html pub fn facility(self, facility: syslog::Facility) -> Self { let mut s = self; s.facility = Some(facility); @@ -208,72 +565,123 @@ impl SyslogBuilder { /// Filter Syslog by level pub fn level(self, lvl: slog::Level) -> Self { let mut s = self; - s.level = level_to_severity(lvl); + s.level = Some(lvl); s } + /// Set a custom hostname, instead of detecting it. + pub fn hostname(mut self, hostname: impl Into) -> Self { + self.hostname = Some(hostname.into()); + self + } + + /// Set a custom process ID, instead of detecting it. + pub fn pid(mut self, pid: i32) -> Self { + self.pid = Some(pid); + self + } + + /// Set the name of this process, instead of detecting it. + pub fn process(mut self, process: impl Into) -> Self { + self.process = Some(process.into()); + self + } + + /// Set the `MsgFormat3164` to use for formatting key-value pairs in log messages. + pub fn msg_format(self, msg_format: F2) -> SyslogBuilder { + // This changes the `F` type parameter of this `SyslogBuilder`, so we can't just change the `msg_format` field. We have to make a whole new `SyslogBuilder` with the new `msg_format`. + SyslogBuilder { + facility: self.facility, + hostname: self.hostname, + level: self.level, + logkind: self.logkind, + msg_format, + pid: self.pid, + process: self.process, + } + } + /// Remote UDP syslogging - pub fn udp>(self, local: SocketAddr, host: SocketAddr, hostname: S) -> Self { + pub fn udp(self, local: SocketAddr, host: SocketAddr) -> Self { let mut s = self; - let hostname = hostname.as_ref().to_string(); - s.logkind = Some(SyslogKind::Udp { + s.logkind = SyslogKind::Udp { local, host, - hostname, - }); + }; s } /// Remote TCP syslogging - pub fn tcp>(self, server: SocketAddr, hostname: S) -> Self { + pub fn tcp(self, server: SocketAddr) -> Self { let mut s = self; - let hostname = hostname.as_ref().to_string(); - s.logkind = Some(SyslogKind::Tcp { server, hostname }); + s.logkind = SyslogKind::Tcp { server }; s } /// Local syslogging over a unix socket - pub fn unix>(self, path: P) -> Self { + pub fn unix(self, path: impl Into) -> Self { let mut s = self; - let path = path.as_ref().to_path_buf(); - s.logkind = Some(SyslogKind::Unix { path }); + let path = path.into(); + s.logkind = 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", - )); - } + /// + /// This method wraps the created `Streamer3164` in a `Mutex`. (For an explanation of why, see the `Streamer3164` documentation.) To get a `Streamer3164` without a `Mutex` wrapper, use the `start_single_threaded` method instead. + pub fn start(self) -> syslog::Result>> { + self.start_single_threaded().map(|streamer| Mutex::new(streamer)) + } + + /// Start running, without wrapping the `Streamer3164` in a `Mutex`. + /// + /// Use this if you plan to use [slog-async] or some other synchronization mechanism. Otherwise, use the `start` method instead. + /// + /// [slog-async]: https://docs.rs/slog-async/2/slog_async/index.html + pub fn start_single_threaded(self) -> syslog::Result> { + let formatter = syslog::Formatter3164 { + facility: self.facility.unwrap_or(Facility::LOG_USER), + hostname: match self.hostname { + some @ Some(_) => some, + None => match hostname::get() { + Ok(hostname) => Some(hostname.to_string_lossy().to_string()), + Err(_) => None + } + }, + pid: self.pid.unwrap_or_else(|| process::id() as i32), + process: match self.process { + Some(process) => process, + None => { + let exe = syslog::ResultExt::chain_err(env::current_exe(), || syslog::ErrorKind::Initialization)?; + + let file_name = exe.file_name().ok_or_else(|| { + let error_msg: Box = Box::from("couldn't get name of this process"); + + syslog::Error::with_boxed_chain(error_msg, syslog::ErrorKind::Initialization) + })?; + + file_name.to_string_lossy().to_string() + } + }, }; - let log = match logkind { - SyslogKind::Unix { path } => syslog::unix_custom(facility, path)?, + let log = match self.logkind { + SyslogKind::UnixDefault => syslog::unix(formatter)?, + SyslogKind::Unix { path } => syslog::unix_custom(formatter, path)?, SyslogKind::Udp { local, host, - hostname, - } => syslog::udp(local, host, hostname, facility)?, - SyslogKind::Tcp { server, hostname } => syslog::tcp(server, hostname, facility)?, + } => syslog::udp(formatter, local, host)?, + SyslogKind::Tcp { server } => syslog::tcp(formatter, server)?, }; - Ok(Streamer3164::new(log)) + Ok(Streamer3164::new(log).with_msg_format(self.msg_format)) } } -/// `Streamer` to Unix syslog using RFC 3164 format -pub fn unix_3164(facility: syslog::Facility) -> io::Result { - syslog::unix(facility).map(Streamer3164::new) +/// `Streamer3164` to local syslog daemon using RFC 3164 format +/// +/// For more control over the created `Streamer3164`, use [`SyslogBuilder`]. +/// +/// [`SyslogBuilder`]: struct.SyslogBuilder.html +pub fn unix_3164(facility: syslog::Facility) -> syslog::Result>> { + SyslogBuilder::new().facility(facility).start() } diff --git a/tests/test.rs b/tests/test.rs new file mode 100644 index 0000000..5a3f565 --- /dev/null +++ b/tests/test.rs @@ -0,0 +1,177 @@ +extern crate slog; +extern crate slog_syslog; +extern crate syslog; + +use slog::*; +use slog_syslog::*; +use std::net::{SocketAddr, UdpSocket}; +use std::sync::Mutex; +use std::thread; +use std::time::Duration; + +mod test_server { + use super::*; + + #[derive(Debug)] + #[must_use = "the test server does nothing useful unless you send something to it"] + pub struct TestServer { + pub server_addr: SocketAddr, + server_thread: Option>>>, + pub client_addr: SocketAddr, + } + + impl TestServer { + pub fn new() -> Self { + let server_socket = UdpSocket::bind("localhost:0").expect("couldn't bind server socket"); + server_socket.set_read_timeout(Some(Duration::from_secs(10))).expect("couldn't set server socket read timeout"); + + let server_addr = server_socket.local_addr().expect("couldn't get server socket address"); + + let client_addr = { + let mut client_addr = server_addr.clone(); + client_addr.set_port(0); + client_addr + }; + + let server_thread = Some(thread::spawn(move || { + let mut packets = Vec::>::new(); + let mut buf = [0u8; 65535]; + + loop { + let (pkt_size, _) = server_socket.recv_from(&mut buf).expect("server couldn't receive packet"); + + if pkt_size == 4 && &buf[0..4] == b"STOP" { + break; + } + + packets.push(Box::from(&buf[..pkt_size])); + } + + packets + })); + + TestServer { server_thread, server_addr, client_addr } + } + + pub fn finish(mut self) -> Vec> { + let server_thread = self.server_thread.take().expect("server thread already stopped"); + + { + let client_socket = UdpSocket::bind(self.client_addr).expect("couldn't bind client socket"); + client_socket.send_to(b"STOP", &self.server_addr).expect("couldn't send stop packet"); + } + + server_thread.join().expect("server thread panicked") + } + } + + impl Drop for TestServer { + fn drop(&mut self) { + if self.server_thread.is_some() { + // Try to stop the server thread. Ignore errors, since this will probably only happen when an error or panic has already occurred. + if let Ok(client_socket) = UdpSocket::bind(self.client_addr) { + let _ = client_socket.send_to(b"STOP", &self.server_addr); + } + } + } + } +} +use test_server::TestServer; + +#[test] +fn integration_test() { + let server = TestServer::new(); + + { + // Set up a logger. + let logger = Logger::root_typed( + Mutex::new(Streamer3164::new(syslog::udp( + syslog::Formatter3164 { + facility: Facility::LOG_USER, + hostname: Some("test-hostname".to_string()), + process: "test-app".to_string(), + pid: 123 + }, + &server.client_addr, + &server.server_addr + ).expect("couldn't create syslog logger"))).fuse(), + o!("key" => "value") + ); + + // Log a test message. + info!(logger, "Hello, world!"; "key2" => "value2"); + } + + // Get the logs received by the server thread. + let logs = server.finish(); + + // Check that the logs were correct. + assert_eq!(logs.len(), 1); + + let s = String::from_utf8(logs[0].to_vec()).expect("log packet contains invalid UTF-8"); + assert!(s.starts_with("<14>")); + assert!(s.ends_with("test-hostname test-app[123]: Hello, world! [key=\"value\" key2=\"value2\"]")); +} + +#[test] +fn integration_test_with_builder() { + let server = TestServer::new(); + + { + // Set up a logger. + let drain = SyslogBuilder::new() + .hostname("test-hostname") + .process("test-app") + .pid(123) + .udp(server.client_addr, server.server_addr) + .start() + .expect("couldn't create syslog logger"); + + let logger = Logger::root_typed(drain.fuse(), o!("key" => "value")); + + // Log a test message. + info!(logger, "Hello, world!"; "key2" => "value2"); + } + + // Get the logs received by the server thread. + let logs = server.finish(); + + // Check that the logs were correct. + assert_eq!(logs.len(), 1); + + let s = String::from_utf8(logs[0].to_vec()).expect("log packet contains invalid UTF-8"); + assert!(s.starts_with("<14>")); + assert!(s.ends_with("test-hostname test-app[123]: Hello, world! [key=\"value\" key2=\"value2\"]")); +} + +#[test] +fn integration_test_with_builder_and_msg_format() { + let server = TestServer::new(); + + { + // Set up a logger. + let drain = SyslogBuilder::new() + .hostname("test-hostname") + .process("test-app") + .pid(123) + .udp(server.client_addr, server.server_addr) + .msg_format(NullMsgFormat3164) + .start() + .expect("couldn't create syslog logger"); + + let logger = Logger::root_typed(drain.fuse(), o!("key" => "value")); + + // Log a test message. + info!(logger, "Hello, world!"; "key2" => "value2"); + } + + // Get the logs received by the server thread. + let logs = server.finish(); + + // Check that the logs were correct. + assert_eq!(logs.len(), 1); + + let s = String::from_utf8(logs[0].to_vec()).expect("log packet contains invalid UTF-8"); + assert!(s.starts_with("<14>")); + assert!(s.ends_with("test-hostname test-app[123]: Hello, world!")); +}