diff --git a/CHANGELOG.md b/CHANGELOG.md index 5018fa0..7ea7afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.29.0] - 2024-08-25 + +Revised `SyslogWriter` (-> version bump): introduced builder pattern, +added a configuration option for the message format +(resolves [issue #168](https://github.com/emabee/flexi_logger/issues/168), kudos to [krims0n32](https://github.com/krims0n32)). + +`LoggerHandle::existing_log_files` now also returns a meaningful result if file rotation is not +used. Kudos to [drdo](https://github.com/drdo) for +[discussion 170](https://github.com/emabee/flexi_logger/discussions/170). + ## [0.28.5] - 2024-06-21 Remove unnecessary dependency to `is-terminal`. diff --git a/Cargo.toml b/Cargo.toml index 42e5f40..1e88430 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "flexi_logger" -version = "0.28.5" +version = "0.29.0" authors = ["emabee "] categories = ["development-tools::debugging"] description = """ diff --git a/README.md b/README.md index f1d7e44..4dc544c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ and you use the ```log``` macros to write log lines from your code): ```toml [dependencies] -flexi_logger = "0.28" +flexi_logger = "0.29" log = "0.4" ``` @@ -74,7 +74,7 @@ Make use of the non-default features by specifying them in your `Cargo.toml`, e. ```toml [dependencies] -flexi_logger = { version = "0.28", features = ["async", "specfile", "compress"] } +flexi_logger = { version = "0.29", features = ["async", "specfile", "compress"] } log = "0.4" ``` @@ -82,7 +82,7 @@ or, to get the smallest footprint (and no colors), switch off even the default f ```toml [dependencies] -flexi_logger = { version = "0.28", default_features = false } +flexi_logger = { version = "0.29", default_features = false } log = "0.4" ``` diff --git a/src/logger_handle.rs b/src/logger_handle.rs index 1b424e7..2909204 100644 --- a/src/logger_handle.rs +++ b/src/logger_handle.rs @@ -381,6 +381,20 @@ impl LoggerHandle { pub fn validate_logs(&self, expected: &[(&'static str, &'static str, &'static str)]) { self.writers_handle.primary_writer.validate_logs(expected); } + + // Allows checking the logs written so far to the writer + #[doc(hidden)] + pub fn validate_additional_logs( + &self, + target: &str, + expected: &[(&'static str, &'static str, &'static str)], + ) { + self.writers_handle + .other_writers + .get(target) + .unwrap(/*fail fast*/) + .validate_logs(expected); + } } /// Used in [`LoggerHandle::existing_log_files`]. diff --git a/src/writers.rs b/src/writers.rs index 616f496..17af5f9 100644 --- a/src/writers.rs +++ b/src/writers.rs @@ -1,39 +1,46 @@ -//! Contains the trait [`LogWriter`] for extending `flexi_logger` -//! with additional log writers, -//! and two concrete implementations -//! for writing to files ([`FileLogWriter`]) -//! or to the syslog ([`SyslogWriter`]). -//! You can also use your own implementations of [`LogWriter`]. +//! Describes how to extend `flexi_logger` with additional log writers +//! (implementations of the trait [`LogWriter`]), and contains two ready-to-use log writers, +//! one for writing to files ([`FileLogWriter`]), one for writing to the syslog ([`SyslogWriter`]). //! -//! Such log writers can be used in two ways: +//! Log writers can be used in two ways: //! -//! * You can influence to which output stream normal log messages will be written, -//! i.e. those from normal log macro calls without explicit target specification. -//! By default, the logs are sent to stderr. With one of the methods +//! * _Default output channel:_
+//! You can influence to which output stream normal log messages will be written, +//! i.e. those from log macro calls without explicit target specification +//! (like in `log::error!("File not found")`). //! +//! With one of the methods +//! +//! * [`Logger::log_to_stderr`](crate::Logger::log_to_stderr) (default) //! * [`Logger::log_to_stdout`](crate::Logger::log_to_stdout) //! * [`Logger::log_to_file`](crate::Logger::log_to_file) //! * [`Logger::log_to_writer`](crate::Logger::log_to_writer) //! * [`Logger::log_to_file_and_writer`](crate::Logger::log_to_file_and_writer) //! * [`Logger::do_not_log`](crate::Logger::do_not_log) //! -//! you can specify a different log target. See there for more details. +//! you can change the default output channel. The fourth and the fifth of these methods +//! take log writers as input. See their documentation for more details. +//! +//! Messages will only be written to the default output channel +//! if they match the current [log specification](crate::LogSpecification). //! -//! Normal log calls will only be written to the chosen output channel if they match the current -//! [log specification](crate::LogSpecification). +//!
//! -//! * You can register additional log writers under a target name with +//! * _Additional output channels:_
+//! You can register additional log writers under a _target name_ with //! [`Logger::add_writer()`](crate::Logger::add_writer), and address these log writers by -//! specifying the target name in calls to the +//! specifying the _target name_ in calls to the //! [log macros](https://docs.rs/log/latest/log/macro.log.html). //! -//! A log call with a target value that has the form `{Name1,Name2,...}`, i.e., -//! a comma-separated list of target names, within braces, is not sent to the default logger, -//! but to the loggers specified explicitly in the list. -//! In such a list you can again specify the default logger with the target name `_Default`. +//! The message of a log call with a _target value_ that has the form `{Name1,Name2,...}`, i.e., +//! a comma-separated list of _target names_, within braces, is not sent to the default output +//! channel, but to the loggers specified explicitly in the list. In such a list +//! you can also specify the default output channel with the built-in target name `_Default`. //! -//! These log calls will not be affected by the value of `flexi_logger`'s log specification; -//! they will always be written, as you might want it for alerts or auditing. +//! Log calls that are directed to an additional output channel will not be affected by +//! the value of `flexi_logger`'s log specification; +//! they will always be handed over to the respective `LogWriter`, +//! as you might want it for alerts or auditing. //! //! In the following example we define an alert writer, and a macro to facilitate using it //! (and avoid using the explicit target specification in the macro call), and @@ -103,12 +110,13 @@ mod log_writer; #[cfg(feature = "syslog_writer")] #[cfg_attr(docsrs, doc(cfg(feature = "syslog_writer")))] -mod syslog_writer; +mod syslog; #[cfg(feature = "syslog_writer")] #[cfg_attr(docsrs, doc(cfg(feature = "syslog_writer")))] -pub use self::syslog_writer::{ - LevelToSyslogSeverity, Syslog, SyslogFacility, SyslogSeverity, SyslogWriter, +pub use self::syslog::{ + syslog_default_format, syslog_format_with_thread, LevelToSyslogSeverity, SyslogConnection, + SyslogFacility, SyslogLineHeader, SyslogSeverity, SyslogWriter, SyslogWriterBuilder, }; pub use self::file_log_writer::{ diff --git a/src/writers/syslog.rs b/src/writers/syslog.rs new file mode 100644 index 0000000..b263528 --- /dev/null +++ b/src/writers/syslog.rs @@ -0,0 +1,19 @@ +mod builder; +mod connection; +mod facility; +mod formats; +mod line; +mod severity; +mod syslog_connection; +mod writer; + +#[allow(clippy::module_name_repetitions)] +pub use self::{ + builder::SyslogWriterBuilder, + facility::SyslogFacility, + formats::{syslog_default_format, syslog_format_with_thread}, + line::SyslogLineHeader, + severity::{LevelToSyslogSeverity, SyslogSeverity}, + syslog_connection::SyslogConnection, + writer::SyslogWriter, +}; diff --git a/src/writers/syslog/builder.rs b/src/writers/syslog/builder.rs new file mode 100644 index 0000000..fced7b9 --- /dev/null +++ b/src/writers/syslog/builder.rs @@ -0,0 +1,90 @@ +use super::{ + line::SyslogLineHeader, severity::default_mapping, syslog_default_format, + LevelToSyslogSeverity, SyslogConnection, SyslogFacility, SyslogWriter, +}; +use crate::FormatFunction; +use std::io::{Error as IoError, ErrorKind, Result as IoResult}; + +#[allow(clippy::module_name_repetitions)] +/// Builder for the `SyslogWriter`. +/// +/// Is created with [`SyslogWriter::builder`]. +pub struct SyslogWriterBuilder { + syslog_connection: SyslogConnection, + syslog_line_header: SyslogLineHeader, + syslog_facility: SyslogFacility, + determine_severity: LevelToSyslogSeverity, + max_log_level: log::LevelFilter, + format: FormatFunction, +} +impl SyslogWriterBuilder { + #[must_use] + pub(super) fn new( + syslog: SyslogConnection, + syslog_line_header: SyslogLineHeader, + syslog_facility: SyslogFacility, + ) -> SyslogWriterBuilder { + SyslogWriterBuilder { + syslog_connection: syslog, + syslog_line_header, + syslog_facility, + determine_severity: default_mapping, + max_log_level: log::LevelFilter::Warn, + format: syslog_default_format, + } + } + + /// Use the given function to map the rust log levels to the syslog severities. + /// By default a trivial mapping is used, which should be good enough in most cases. + #[must_use] + pub fn determine_severity(mut self, mapping: LevelToSyslogSeverity) -> Self { + self.determine_severity = mapping; + self + } + + /// Specify up to which level log messages should be sent to the syslog. + /// + /// Default is: only warnings and errors. + #[must_use] + pub fn max_log_level(mut self, max_log_level: log::LevelFilter) -> Self { + self.max_log_level = max_log_level; + self + } + + /// Use the given format function to write the message part of the syslog entries. + /// + /// By default, [`syslog_default_format`](crate::writers::syslog_default_format) is used. + /// + /// You can instead use [`syslog_format_with_thread`](crate::writers::syslog_format_with_thread) + /// or your own `FormatFunction` + /// (see the source code of the provided functions if you want to write your own). + #[must_use] + pub fn format(mut self, format: FormatFunction) -> Self { + self.format = format; + self + } + + /// Returns a boxed instance of `SysLogWriter`. + /// + /// # Errors + /// + /// `std::io::Error` if the program's argument list is empty so that the process + /// identifier for the syslog cannot be determined + pub fn build(self) -> IoResult> { + Ok(Box::new(SyslogWriter::new( + std::process::id(), + std::env::args().next().ok_or_else(|| { + IoError::new( + ErrorKind::Other, + "Can't infer app name as no env args are present".to_owned(), + ) + })?, + self.syslog_line_header, + self.syslog_facility, + self.determine_severity, + self.syslog_connection, + self.max_log_level, + self.format, + )?)) + } +} diff --git a/src/writers/syslog/connection.rs b/src/writers/syslog/connection.rs new file mode 100644 index 0000000..75d6949 --- /dev/null +++ b/src/writers/syslog/connection.rs @@ -0,0 +1,69 @@ +use std::{ + io::{Result as IoResult, Write}, + net::{TcpStream, UdpSocket}, +}; + +// Writable and flushable connection to the syslog backend. +#[derive(Debug)] +pub(super) enum Connection { + // Sends log lines to the syslog via a + // [UnixStream](https://doc.rust-lang.org/std/os/unix/net/struct.UnixStream.html). + #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] + #[cfg(target_family = "unix")] + Stream(std::os::unix::net::UnixStream), + + // Sends log lines to the syslog via a + // [UnixDatagram](https://doc.rust-lang.org/std/os/unix/net/struct.UnixDatagram.html). + #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] + #[cfg(target_family = "unix")] + Datagram(std::os::unix::net::UnixDatagram), + + // Sends log lines to the syslog via UDP. + // + // UDP is fragile and thus discouraged except for local communication. + Udp(UdpSocket), + + // Sends log lines to the syslog via TCP. + Tcp(TcpStream), +} + +impl Write for Connection { + fn write(&mut self, buf: &[u8]) -> IoResult { + match *self { + #[cfg(target_family = "unix")] + Self::Datagram(ref ud) => { + // todo: reconnect if conn is broken + ud.send(buf) + } + #[cfg(target_family = "unix")] + Self::Stream(ref mut w) => { + // todo: reconnect if conn is broken + w.write(buf) + .and_then(|sz| w.write_all(&[0; 1]).map(|()| sz)) + } + Self::Tcp(ref mut w) => { + // todo: reconnect if conn is broken + let n = w.write(buf)?; + Ok(w.write(b"\n")? + n) + } + Self::Udp(ref socket) => { + // ?? + socket.send(buf) + } + } + } + + fn flush(&mut self) -> IoResult<()> { + match *self { + #[cfg(target_family = "unix")] + Self::Datagram(_) => Ok(()), + + #[cfg(target_family = "unix")] + Self::Stream(ref mut w) => w.flush(), + + Self::Udp(_) => Ok(()), + + Self::Tcp(ref mut w) => w.flush(), + } + } +} diff --git a/src/writers/syslog/facility.rs b/src/writers/syslog/facility.rs new file mode 100644 index 0000000..32b4f58 --- /dev/null +++ b/src/writers/syslog/facility.rs @@ -0,0 +1,55 @@ +/// Syslog Facility, according to [RFC 5424](https://datatracker.ietf.org/doc/rfc5424). +/// +/// Note that the original integer values are already multiplied by 8. +#[derive(Copy, Clone, Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum SyslogFacility { + /// kernel messages. + Kernel = 0 << 3, + /// user-level messages. + UserLevel = 1 << 3, + /// mail system. + MailSystem = 2 << 3, + /// system daemons. + SystemDaemons = 3 << 3, + /// security/authorization messages. + Authorization = 4 << 3, + /// messages generated internally by syslogd. + SyslogD = 5 << 3, + /// line printer subsystem. + LinePrinter = 6 << 3, + /// network news subsystem. + News = 7 << 3, + /// UUCP subsystem. + Uucp = 8 << 3, + /// clock daemon. + Clock = 9 << 3, + /// security/authorization messages. + Authorization2 = 10 << 3, + /// FTP daemon. + Ftp = 11 << 3, + /// NTP subsystem. + Ntp = 12 << 3, + /// log audit. + LogAudit = 13 << 3, + /// log alert. + LogAlert = 14 << 3, + /// clock daemon (note 2). + Clock2 = 15 << 3, + /// local use 0 (local0). + LocalUse0 = 16 << 3, + /// local use 1 (local1). + LocalUse1 = 17 << 3, + /// local use 2 (local2). + LocalUse2 = 18 << 3, + /// local use 3 (local3). + LocalUse3 = 19 << 3, + /// local use 4 (local4). + LocalUse4 = 20 << 3, + /// local use 5 (local5). + LocalUse5 = 21 << 3, + /// local use 6 (local6). + LocalUse6 = 22 << 3, + /// local use 7 (local7). + LocalUse7 = 23 << 3, +} diff --git a/src/writers/syslog/formats.rs b/src/writers/syslog/formats.rs new file mode 100644 index 0000000..9e81f4d --- /dev/null +++ b/src/writers/syslog/formats.rs @@ -0,0 +1,30 @@ +use crate::DeferredNow; +use log::Record; + +/// Default way of writing the message to the syslog. +/// +/// Just uses the `Display` implementation of `record.args()`. +#[allow(clippy::missing_errors_doc)] +pub fn syslog_default_format( + w: &mut dyn std::io::Write, + _now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error> { + write!(w, "{}", record.args()) +} + +/// Similar to `syslog_default_format`, but inserts the thread name in the beginning of the message, +/// encapsulated in square brackets. +#[allow(clippy::missing_errors_doc)] +pub fn syslog_format_with_thread( + w: &mut dyn std::io::Write, + _now: &mut DeferredNow, + record: &Record, +) -> Result<(), std::io::Error> { + write!( + w, + "[{}] {}", + std::thread::current().name().unwrap_or(""), + record.args() + ) +} diff --git a/src/writers/syslog/line.rs b/src/writers/syslog/line.rs new file mode 100644 index 0000000..dc3b925 --- /dev/null +++ b/src/writers/syslog/line.rs @@ -0,0 +1,137 @@ +use std::io::{Error as IoError, ErrorKind, Result as IoResult, Write}; + +use crate::{DeferredNow, FormatFunction}; + +use super::{LevelToSyslogSeverity, SyslogFacility}; + +/// Defines the format of the header of a syslog line. +pub enum SyslogLineHeader { + /// Line header according to RFC 5424. + Rfc5424(String), + /// Line header according to RFC 3164. + Rfc3164, +} +pub(crate) struct LineWriter { + header: SyslogLineHeader, + hostname: String, + process: String, + pid: u32, + format: FormatFunction, + determine_severity: LevelToSyslogSeverity, + facility: SyslogFacility, +} +impl LineWriter { + pub(crate) fn new( + header: SyslogLineHeader, + determine_severity: LevelToSyslogSeverity, + facility: SyslogFacility, + process: String, + pid: u32, + format: FormatFunction, + ) -> IoResult { + const UNKNOWN_HOSTNAME: &str = ""; + // FIXME + Ok(LineWriter { + header, + hostname: hostname::get().map_or_else( + |_| Ok(UNKNOWN_HOSTNAME.to_owned()), + |s| { + s.into_string().map_err(|_| { + IoError::new( + ErrorKind::InvalidData, + "Hostname contains non-UTF8 characters".to_owned(), + ) + }) + }, + )?, + process, + pid, + format, + determine_severity, + facility, + }) + } + + pub(crate) fn write_syslog_entry( + &self, + buffer: &mut dyn Write, + now: &mut DeferredNow, + record: &log::Record, + ) -> IoResult<()> { + // See [RFC 5424](https://datatracker.ietf.org/doc/rfc5424#page-8). + let severity = (self.determine_severity)(record.level()); + + match self.header { + SyslogLineHeader::Rfc3164 => { + write!( + buffer, + "<{pri}>{timestamp} {tag}[{procid}]: ", + pri = self.facility as u8 | severity as u8, + timestamp = now.format_rfc3164(), + tag = self.process, + procid = self.pid + )?; + (self.format)(buffer, now, record)?; + } + SyslogLineHeader::Rfc5424(ref message_id) => { + #[allow(clippy::write_literal)] + write!( + buffer, + "<{pri}>{version} {timestamp} {hostname} {appname} {procid} {msgid} ", + pri = self.facility as u8 | severity as u8, + version = "1", + timestamp = now.format_rfc3339(), + hostname = self.hostname, + appname = self.process, + procid = self.pid, + msgid = message_id, + )?; + write_key_value_pairs(buffer, record)?; + (self.format)(buffer, now, record)?; + } + } + Ok(()) + } +} + +// Helpers for printing key-value pairs +fn write_key_value_pairs( + w: &mut dyn std::io::Write, + record: &log::Record<'_>, +) -> Result<(), std::io::Error> { + let mut kv_written = false; + #[cfg(feature = "kv")] + if record.key_values().count() > 0 { + write!(w, "[log_kv ",)?; + let mut kv_stream = KvStream(w, false); + record.key_values().visit(&mut kv_stream).ok(); + write!(w, "] ")?; + kv_written = true; + } + + if !kv_written { + write!(w, "- ")?; + } + Ok(()) +} + +#[cfg(feature = "kv")] +struct KvStream<'a>(&'a mut dyn std::io::Write, bool); +#[cfg(feature = "kv")] +impl<'kvs, 'a> log::kv::VisitSource<'kvs> for KvStream<'a> +where + 'kvs: 'a, +{ + fn visit_pair( + &mut self, + key: log::kv::Key<'kvs>, + value: log::kv::Value<'kvs>, + ) -> Result<(), log::kv::Error> { + if self.1 { + write!(self.0, " ")?; + } + write!(self.0, "{key}=\"{value:?}\"")?; + self.1 = true; + Ok(()) + } +} diff --git a/src/writers/syslog/severity.rs b/src/writers/syslog/severity.rs new file mode 100644 index 0000000..8e2a153 --- /dev/null +++ b/src/writers/syslog/severity.rs @@ -0,0 +1,37 @@ +/// Syslog severity. +/// +/// See [RFC 5424](https://datatracker.ietf.org/doc/rfc5424). +#[derive(Debug)] +#[allow(clippy::module_name_repetitions)] +pub enum SyslogSeverity { + /// System is unusable. + Emergency = 0, + /// Action must be taken immediately. + Alert = 1, + /// Critical conditions. + Critical = 2, + /// Error conditions. + Error = 3, + /// Warning conditions + Warning = 4, + /// Normal but significant condition + Notice = 5, + /// Informational messages. + Info = 6, + /// Debug-level messages. + Debug = 7, +} + +/// Signature for a custom mapping function that maps the rust log levels to +/// values of the syslog Severity. +#[allow(clippy::module_name_repetitions)] +pub type LevelToSyslogSeverity = fn(level: log::Level) -> SyslogSeverity; + +pub(crate) fn default_mapping(level: log::Level) -> SyslogSeverity { + match level { + log::Level::Error => SyslogSeverity::Error, + log::Level::Warn => SyslogSeverity::Warning, + log::Level::Info => SyslogSeverity::Info, + log::Level::Debug | log::Level::Trace => SyslogSeverity::Debug, + } +} diff --git a/src/writers/syslog/syslog_connection.rs b/src/writers/syslog/syslog_connection.rs new file mode 100644 index 0000000..26e4823 --- /dev/null +++ b/src/writers/syslog/syslog_connection.rs @@ -0,0 +1,71 @@ +use super::connection::Connection; +#[cfg(target_family = "unix")] +use std::path::Path; +use std::{ + io::Result as IoResult, + net::{TcpStream, ToSocketAddrs, UdpSocket}, +}; + +/// Implements the connection to the syslog. +/// +/// Choose one of the factory methods that matches your environment, +/// depending on how the syslog is managed on your system, +/// how you can access it and with which protocol you can write to it. +/// +/// Is required to instantiate a [`SyslogWriter`](crate::writers::SyslogWriter). +#[allow(clippy::module_name_repetitions)] +pub struct SyslogConnection(Connection); +impl SyslogConnection { + /// Returns a `Syslog` that connects via unix datagram to the specified path. + /// + /// # Errors + /// + /// Any kind of I/O error can occur. + #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] + #[cfg(target_family = "unix")] + pub fn try_datagram>(path: P) -> IoResult { + let ud = std::os::unix::net::UnixDatagram::unbound()?; + ud.connect(&path)?; + Ok(SyslogConnection(Connection::Datagram(ud))) + } + + /// Returns a `Syslog` that connects via unix stream to the specified path. + /// + /// # Errors + /// + /// Any kind of I/O error can occur. + #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] + #[cfg(target_family = "unix")] + pub fn try_stream>(path: P) -> IoResult { + Ok(SyslogConnection(Connection::Stream( + std::os::unix::net::UnixStream::connect(path)?, + ))) + } + + /// Returns a `Syslog` that sends the log lines via TCP to the specified address. + /// + /// # Errors + /// + /// `std::io::Error` if opening the stream fails. + pub fn try_tcp(server: T) -> IoResult { + Ok(SyslogConnection(Connection::Tcp(TcpStream::connect( + server, + )?))) + } + + /// Returns a `Syslog` that sends the log via the fragile UDP protocol from local + /// to server. + /// + /// # Errors + /// + /// `std::io::Error` if opening the stream fails. + pub fn try_udp(local: T, server: T) -> IoResult { + let socket = UdpSocket::bind(local)?; + socket.connect(server)?; + Ok(SyslogConnection(Connection::Udp(socket))) + } + + pub(super) fn into_inner(self) -> Connection { + self.0 + } +} diff --git a/src/writers/syslog/writer.rs b/src/writers/syslog/writer.rs new file mode 100644 index 0000000..bb688aa --- /dev/null +++ b/src/writers/syslog/writer.rs @@ -0,0 +1,276 @@ +use super::{ + connection::Connection, line::LineWriter, LevelToSyslogSeverity, SyslogConnection, + SyslogFacility, SyslogLineHeader, SyslogWriterBuilder, +}; +use crate::{writers::log_writer::LogWriter, DeferredNow, FormatFunction}; +#[cfg(test)] +use std::io::BufRead; +use std::{ + io::{Cursor, Result as IoResult, Write}, + sync::Mutex, +}; + +/// A configurable [`LogWriter`] implementation that writes log messages to the syslog. +/// +/// Only available with optional crate feature `syslog_writer`. +/// +/// See the [writers](crate::writers) module for guidance how to use additional log writers. +#[allow(clippy::module_name_repetitions)] +pub struct SyslogWriter { + line_writer: LineWriter, + m_conn_buf: Mutex, + max_log_level: log::LevelFilter, + #[cfg(test)] + validation_buffer: Mutex>>, +} +impl SyslogWriter { + /// Instantiate the builder for the `SysLogWriter`. + #[must_use] + pub fn builder( + syslog: SyslogConnection, + syslog_line_header: SyslogLineHeader, + syslog_facility: SyslogFacility, + ) -> SyslogWriterBuilder { + SyslogWriterBuilder::new(syslog, syslog_line_header, syslog_facility) + } + #[allow(clippy::too_many_arguments)] + pub(super) fn new( + pid: u32, + process: String, + syslog_line_header: SyslogLineHeader, + facility: SyslogFacility, + determine_severity: LevelToSyslogSeverity, + syslog_connection: SyslogConnection, + max_log_level: log::LevelFilter, + format: FormatFunction, + ) -> IoResult { + Ok(SyslogWriter { + line_writer: LineWriter::new( + syslog_line_header, + determine_severity, + facility, + process, + pid, + format, + )?, + m_conn_buf: Mutex::new(ConnectorAndBuffer { + conn: syslog_connection.into_inner(), + buf: Vec::with_capacity(200), + }), + max_log_level, + #[cfg(test)] + validation_buffer: Mutex::new(Cursor::new(Vec::new())), + }) + } +} +impl LogWriter for SyslogWriter { + fn write(&self, now: &mut DeferredNow, record: &log::Record) -> IoResult<()> { + let mut conn_buf_guard = self + .m_conn_buf + .lock() + .map_err(|_| crate::util::io_err("SyslogWriter is poisoned"))?; + let cb = &mut *conn_buf_guard; + cb.buf.clear(); + let mut buffer = Cursor::new(&mut cb.buf); + + self.line_writer + .write_syslog_entry(&mut buffer, now, record)?; + + #[cfg(test)] + { + let mut valbuf = self.validation_buffer.lock().unwrap(); + valbuf.write_all(&cb.buf)?; + valbuf.write_all(b"\n")?; + } + + // we _have_ to buffer above because each write here generates a syslog entry + cb.conn.write_all(&cb.buf) + } + + fn flush(&self) -> IoResult<()> { + self.m_conn_buf + .lock() + .map_err(|_| crate::util::io_err("SyslogWriter is poisoned"))? + .conn + .flush() + } + + fn max_log_level(&self) -> log::LevelFilter { + self.max_log_level + } + + #[doc(hidden)] + fn validate_logs(&self, _expected: &[(&'static str, &'static str, &'static str)]) { + #[cfg(test)] + { + let write_cursor = self.validation_buffer.lock().unwrap(); + let mut reader = std::io::BufReader::new(Cursor::new(write_cursor.get_ref())); + let mut buf = String::new(); + #[allow(clippy::used_underscore_binding)] + for tuple in _expected { + buf.clear(); + reader.read_line(&mut buf).unwrap(); + assert!( + buf.contains(tuple.0), + "Did not find tuple.0 = {} in {}", + tuple.0, + buf + ); + assert!(buf.contains(tuple.1), "Did not find tuple.1 = {}", tuple.1); + assert!(buf.contains(tuple.2), "Did not find tuple.2 = {}", tuple.2); + } + buf.clear(); + reader.read_line(&mut buf).unwrap(); + assert!(buf.is_empty(), "Found more log lines than expected: {buf} ",); + } + } +} + +struct ConnectorAndBuffer { + conn: Connection, + buf: Vec, +} + +///////////////////////////// + +#[cfg(test)] +mod test { + + use crate::{ + detailed_format, + writers::{ + syslog_format_with_thread, SyslogConnection, SyslogFacility, SyslogLineHeader, + SyslogWriter, + }, + FileSpec, Logger, + }; + use chrono::{DateTime, Local}; + use log::*; + use std::path::PathBuf; + + #[doc(hidden)] + #[macro_use] + mod macros { + #[macro_export] + macro_rules! syslog1 { + ($($arg:tt)*) => ( + error!(target: "{Syslog1,_Default}", $($arg)*); + ) + } + #[macro_export] + macro_rules! syslog2 { + ($($arg:tt)*) => ( + error!(target: "{Syslog2,_Default}", $($arg)*); + ) + } + } + + #[test] + fn test_syslog() { + let boxed_syslog_writer1 = SyslogWriter::builder( + SyslogConnection::try_udp("127.0.0.1:5555", "127.0.0.1:514").unwrap(), + SyslogLineHeader::Rfc5424("JustForTest".to_owned()), + SyslogFacility::LocalUse0, + ) + .max_log_level(log::LevelFilter::Trace) + .build() + .unwrap(); + + let boxed_syslog_writer2 = SyslogWriter::builder( + SyslogConnection::try_udp("127.0.0.1:5556", "127.0.0.1:514").unwrap(), + SyslogLineHeader::Rfc3164, + SyslogFacility::LocalUse0, + ) + .max_log_level(log::LevelFilter::Trace) + .format(syslog_format_with_thread) + .build() + .unwrap(); + + let logger = Logger::try_with_str("info") + .unwrap() + .format(detailed_format) + .log_to_file(FileSpec::default().suppress_timestamp().directory(dir())) + .print_message() + .add_writer("Syslog1", boxed_syslog_writer1) + .add_writer("Syslog2", boxed_syslog_writer2) + .start() + .unwrap_or_else(|e| panic!("Logger initialization failed with {e}")); + + // Explicitly send logs to different loggers + error!(target : "{Syslog1}", "This is a syslog-relevant error msg"); + warn!(target : "{Syslog1}", "This is a syslog-relevant warn msg"); + info!(target : "{Syslog1}", "This is a syslog-relevant info msg"); + debug!(target : "{Syslog1}", "This is a syslog-relevant debug msg"); + trace!(target : "{Syslog1}", "This is a syslog-relevant trace msg"); + + error!(target : "{Syslog1,_Default}", "This is a syslog- and log-relevant msg"); + + error!(target : "{Syslog2}", "This is a syslog-relevant error msg"); + warn!(target : "{Syslog2}", "This is a syslog-relevant warn msg"); + info!(target : "{Syslog2}", "This is a syslog-relevant info msg"); + debug!(target : "{Syslog2}", "This is a syslog-relevant debug msg"); + trace!(target : "{Syslog2}", "This is a syslog-relevant trace msg"); + + error!(target : "{Syslog2,_Default}", "This is a syslog- and log-relevant msg"); + + // Nicer: use explicit macros + syslog1!("This is another syslog- and log error msg"); + syslog2!("This is one more syslog- and log error msg"); + warn!("This is a warning message"); + debug!("This is a debug message - you must not see it!"); + trace!("This is a trace message - you must not see it!"); + + // Verification: + // this only validates the normal log target (file) + logger.validate_logs(&[ + ("ERROR", "", "a syslog- and log-relevant msg"), + ("ERROR", "", "a syslog- and log-relevant msg"), + ("ERROR", "", "another syslog- and log error msg"), + ("ERROR", "", "one more syslog- and log error msg"), + ("WARN", "syslog", "This is a warning message"), + ]); + logger.validate_additional_logs( + "Syslog1", + &[ + ("<131>1", "JustForTest", "is a syslog-relevant error msg"), + ("<132>1", "JustForTest", "is a syslog-relevant warn msg"), + ("<134>1", "JustForTest", "is a syslog-relevant info msg"), + ("<135>1", "JustForTest", "is a syslog-relevant debug msg"), + ("<135>1", "JustForTest", "is a syslog-relevant trace msg"), + ("<131>1", "JustForTest", "is a syslog- and log-relevant msg"), + ("<131>1", "JustForTest", "is another syslog- and log error"), + ], + ); + logger.validate_additional_logs( + "Syslog2", + &[ + ("<131>", "]: [", "This is a syslog-relevant error msg"), + ("<132>", "]: [", "This is a syslog-relevant warn msg"), + ("<134>", "]: [", "This is a syslog-relevant info msg"), + ("<135>", "]: [", "This is a syslog-relevant debug msg"), + ("<135>", "]: [", "This is a syslog-relevant trace msg"), + ("<131>", "]: [", "This is a syslog- and log-relevant msg"), + ("<131>", "]: [", "This is one more syslog- and log error"), + ], + ); + } + + fn dir() -> PathBuf { + let mut d = PathBuf::new(); + d.push("log_files"); + add_prog_name(&mut d); + d.push(now_local().format(TS).to_string()); + d + } + fn add_prog_name(pb: &mut PathBuf) { + let path = PathBuf::from(std::env::args().next().unwrap()); + let filename = path.file_stem().unwrap(/*ok*/).to_string_lossy(); + let (progname, _) = filename.rsplit_once('-').unwrap_or((&filename, "")); + pb.push(progname); + } + #[must_use] + pub fn now_local() -> DateTime { + Local::now() + } + const TS: &str = "%Y-%m-%d_%H-%M-%S"; +} diff --git a/src/writers/syslog_writer.rs b/src/writers/syslog_writer.rs deleted file mode 100644 index 4c512a6..0000000 --- a/src/writers/syslog_writer.rs +++ /dev/null @@ -1,434 +0,0 @@ -use crate::{writers::log_writer::LogWriter, DeferredNow}; -use std::io::{Error as IoError, ErrorKind, Result as IoResult, Write}; -use std::net::{TcpStream, ToSocketAddrs, UdpSocket}; -#[cfg(target_family = "unix")] -use std::path::Path; -use std::sync::Mutex; - -/// Syslog Facility. -/// -/// See [RFC 5424](https://datatracker.ietf.org/doc/rfc5424). -#[derive(Copy, Clone, Debug)] -pub enum SyslogFacility { - /// kernel messages. - Kernel = 0 << 3, - /// user-level messages. - UserLevel = 1 << 3, - /// mail system. - MailSystem = 2 << 3, - /// system daemons. - SystemDaemons = 3 << 3, - /// security/authorization messages. - Authorization = 4 << 3, - /// messages generated internally by syslogd. - SyslogD = 5 << 3, - /// line printer subsystem. - LinePrinter = 6 << 3, - /// network news subsystem. - News = 7 << 3, - /// UUCP subsystem. - Uucp = 8 << 3, - /// clock daemon. - Clock = 9 << 3, - /// security/authorization messages. - Authorization2 = 10 << 3, - /// FTP daemon. - Ftp = 11 << 3, - /// NTP subsystem. - Ntp = 12 << 3, - /// log audit. - LogAudit = 13 << 3, - /// log alert. - LogAlert = 14 << 3, - /// clock daemon (note 2). - Clock2 = 15 << 3, - /// local use 0 (local0). - LocalUse0 = 16 << 3, - /// local use 1 (local1). - LocalUse1 = 17 << 3, - /// local use 2 (local2). - LocalUse2 = 18 << 3, - /// local use 3 (local3). - LocalUse3 = 19 << 3, - /// local use 4 (local4). - LocalUse4 = 20 << 3, - /// local use 5 (local5). - LocalUse5 = 21 << 3, - /// local use 6 (local6). - LocalUse6 = 22 << 3, - /// local use 7 (local7). - LocalUse7 = 23 << 3, -} - -/// Syslog severity. -/// -/// See [RFC 5424](https://datatracker.ietf.org/doc/rfc5424). -#[derive(Debug)] -pub enum SyslogSeverity { - /// System is unusable. - Emergency = 0, - /// Action must be taken immediately. - Alert = 1, - /// Critical conditions. - Critical = 2, - /// Error conditions. - Error = 3, - /// Warning conditions - Warning = 4, - /// Normal but significant condition - Notice = 5, - /// Informational messages. - Info = 6, - /// Debug-level messages. - Debug = 7, -} - -/// Signature for a custom mapping function that maps the rust log levels to -/// values of the syslog Severity. -pub type LevelToSyslogSeverity = fn(level: log::Level) -> SyslogSeverity; - -fn default_mapping(level: log::Level) -> SyslogSeverity { - match level { - log::Level::Error => SyslogSeverity::Error, - log::Level::Warn => SyslogSeverity::Warning, - log::Level::Info => SyslogSeverity::Info, - log::Level::Debug | log::Level::Trace => SyslogSeverity::Debug, - } -} - -enum SyslogType { - Rfc5424 { - hostname: String, - message_id: String, - }, - Rfc3164, -} - -/// A configurable [`LogWriter`] implementation that writes log messages to the syslog -/// (see [RFC 5424](https://datatracker.ietf.org/doc/rfc5424)). -/// -/// Only available with optional crate feature `syslog_writer`. -/// -/// See the [writers](crate::writers) module for guidance how to use additional log writers. -pub struct SyslogWriter { - process: String, - pid: u32, - syslog_type: SyslogType, - facility: SyslogFacility, - determine_severity: LevelToSyslogSeverity, - m_conn_buf: Mutex, - max_log_level: log::LevelFilter, -} -impl SyslogWriter { - /// Returns a configured boxed instance. - /// - /// ## Parameters - /// - /// `facility`: An value representing a valid syslog facility value according to RFC 5424. - /// - /// `determine_severity`: (optional) A function that maps the rust log levels - /// to the syslog severities. If None is given, a trivial default mapping is used, which - /// should be good enough in most cases. - /// - /// `message_id`: The value being used as syslog's MSGID, which - /// should identify the type of message. The value itself - /// is a string without further semantics. It is intended for filtering - /// messages on a relay or collector. - /// - /// `syslog`: A [`Syslog`](crate::writers::Syslog). - /// - /// # Errors - /// - /// `std::io::Error` - pub fn try_new( - facility: SyslogFacility, - determine_severity: Option, - max_log_level: log::LevelFilter, - message_id: String, - syslog: Syslog, - ) -> IoResult> { - const UNKNOWN_HOSTNAME: &str = ""; - - let hostname = hostname::get().map_or_else( - |_| Ok(UNKNOWN_HOSTNAME.to_owned()), - |s| { - s.into_string().map_err(|_| { - IoError::new( - ErrorKind::InvalidData, - "Hostname contains non-UTF8 characters".to_owned(), - ) - }) - }, - )?; - - let process = std::env::args().next().ok_or_else(|| { - IoError::new( - ErrorKind::Other, - "Can't infer app name as no env args are present".to_owned(), - ) - })?; - - Ok(Box::new(Self { - pid: std::process::id(), - process, - syslog_type: SyslogType::Rfc5424 { - hostname, - message_id, - }, - facility, - max_log_level, - // shorter variants with unwrap_or() or unwrap_or_else() don't work - // with either current clippy or old rustc: - determine_severity: match determine_severity { - Some(f) => f, - None => default_mapping, - }, - m_conn_buf: Mutex::new(ConnectorAndBuffer { - conn: syslog.into_inner(), - buf: Vec::with_capacity(200), - }), - })) - } - - /// Returns a configured boxed instance. - /// - /// ## Parameters - /// - /// `facility`: An value representing a valid syslog facility value according to RFC 5424. - /// - /// `determine_severity`: (optional) A function that maps the rust log levels - /// to the syslog severities. If None is given, a trivial default mapping is used, which - /// should be good enough in most cases. - /// - /// `message_id`: The value being used as syslog's MSGID, which - /// should identify the type of message. The value itself - /// is a string without further semantics. It is intended for filtering - /// messages on a relay or collector. - /// - /// `syslog`: A [`Syslog`](crate::writers::Syslog). - /// - /// # Errors - /// - /// `std::io::Error` - pub fn try_new_bsd( - facility: SyslogFacility, - determine_severity: Option, - max_log_level: log::LevelFilter, - syslog: Syslog, - ) -> IoResult> { - let process = std::env::args().next().ok_or_else(|| { - IoError::new( - ErrorKind::Other, - "Can't infer app name as no env args are present".to_owned(), - ) - })?; - - Ok(Box::new(Self { - pid: std::process::id(), - process, - syslog_type: SyslogType::Rfc3164, - facility, - max_log_level, - // shorter variants with unwrap_or() or unwrap_or_else() don't work - // with either current clippy or old rustc: - determine_severity: match determine_severity { - Some(f) => f, - None => default_mapping, - }, - m_conn_buf: Mutex::new(ConnectorAndBuffer { - conn: syslog.into_inner(), - buf: Vec::with_capacity(200), - }), - })) - } -} - -impl LogWriter for SyslogWriter { - fn write(&self, now: &mut DeferredNow, record: &log::Record) -> IoResult<()> { - let mut conn_buf_guard = self - .m_conn_buf - .lock() - .map_err(|_| crate::util::io_err("SyslogWriter is poisoned"))?; - let cb = &mut *conn_buf_guard; - let severity = (self.determine_severity)(record.level()); - - // See [RFC 5424](https://datatracker.ietf.org/doc/rfc5424#page-8). - cb.buf.clear(); - - match &self.syslog_type { - SyslogType::Rfc3164 => { - write!( - cb.buf, - "<{pri}>{timestamp} {tag}[{procid}]: {msg}", - pri = self.facility as u8 | severity as u8, - timestamp = now.format_rfc3164(), - tag = self.process, - procid = self.pid, - msg = &record.args() - )?; - } - SyslogType::Rfc5424 { - hostname, - message_id, - } => { - #[allow(clippy::write_literal)] - write!( - cb.buf, - "<{pri}>{version} {timestamp} {hostname} {appname} {procid} {msgid} - {msg}", - pri = self.facility as u8 | severity as u8, - version = "1", - timestamp = now.format_rfc3339(), - hostname = hostname, - appname = self.process, - procid = self.pid, - msgid = message_id, - msg = &record.args() - )?; - } - } - // we _have_ to buffer because each write here generates a syslog entry - cb.conn.write_all(&cb.buf) - } - - fn flush(&self) -> IoResult<()> { - self.m_conn_buf - .lock() - .map_err(|_| crate::util::io_err("SyslogWriter is poisoned"))? - .conn - .flush() - } - - fn max_log_level(&self) -> log::LevelFilter { - self.max_log_level - } -} - -struct ConnectorAndBuffer { - conn: SyslogConnector, - buf: Vec, -} - -/// Implements the connection to the syslog. -/// -/// Choose one of the factory methods that matches your environment, -/// depending on how the syslog is managed on your system, -/// how you can access it and with which protocol you can write to it. -/// -/// Is required to instantiate a [`SyslogWriter`](crate::writers::SyslogWriter). -pub struct Syslog(SyslogConnector); -impl Syslog { - /// Returns a Syslog implementation that connects via unix datagram to the specified path. - /// - /// # Errors - /// - /// Any kind of I/O error can occur. - #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] - #[cfg(target_family = "unix")] - pub fn try_datagram>(path: P) -> IoResult { - let ud = std::os::unix::net::UnixDatagram::unbound()?; - ud.connect(&path)?; - Ok(Syslog(SyslogConnector::Datagram(ud))) - } - - /// Returns a Syslog implementation that connects via unix stream to the specified path. - /// - /// # Errors - /// - /// Any kind of I/O error can occur. - #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] - #[cfg(target_family = "unix")] - pub fn try_stream>(path: P) -> IoResult { - Ok(Syslog(SyslogConnector::Stream( - std::os::unix::net::UnixStream::connect(path)?, - ))) - } - - /// Returns a Syslog implementation that sends the log lines via TCP to the specified address. - /// - /// # Errors - /// - /// `std::io::Error` if opening the stream fails. - pub fn try_tcp(server: T) -> IoResult { - Ok(Syslog(SyslogConnector::Tcp(TcpStream::connect(server)?))) - } - - /// Returns a Syslog implementation that sends the log via the fragile UDP protocol from local - /// to server. - /// - /// # Errors - /// - /// `std::io::Error` if opening the stream fails. - pub fn try_udp(local: T, server: T) -> IoResult { - let socket = UdpSocket::bind(local)?; - socket.connect(server)?; - Ok(Syslog(SyslogConnector::Udp(socket))) - } - - fn into_inner(self) -> SyslogConnector { - self.0 - } -} - -#[derive(Debug)] -enum SyslogConnector { - // Sends log lines to the syslog via a - // [UnixStream](https://doc.rust-lang.org/std/os/unix/net/struct.UnixStream.html). - #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] - #[cfg(target_family = "unix")] - Stream(std::os::unix::net::UnixStream), - - // Sends log lines to the syslog via a - // [UnixDatagram](https://doc.rust-lang.org/std/os/unix/net/struct.UnixDatagram.html). - #[cfg_attr(docsrs, doc(cfg(target_family = "unix")))] - #[cfg(target_family = "unix")] - Datagram(std::os::unix::net::UnixDatagram), - - // Sends log lines to the syslog via UDP. - // - // UDP is fragile and thus discouraged except for local communication. - Udp(UdpSocket), - - // Sends log lines to the syslog via TCP. - Tcp(TcpStream), -} - -impl Write for SyslogConnector { - fn write(&mut self, buf: &[u8]) -> IoResult { - match *self { - #[cfg(target_family = "unix")] - Self::Datagram(ref ud) => { - // todo: reconnect if conn is broken - ud.send(buf) - } - #[cfg(target_family = "unix")] - Self::Stream(ref mut w) => { - // todo: reconnect if conn is broken - w.write(buf) - .and_then(|sz| w.write_all(&[0; 1]).map(|()| sz)) - } - Self::Tcp(ref mut w) => { - // todo: reconnect if conn is broken - let n = w.write(buf)?; - Ok(w.write(b"\n")? + n) - } - Self::Udp(ref socket) => { - // ?? - socket.send(buf) - } - } - } - - fn flush(&mut self) -> IoResult<()> { - match *self { - #[cfg(target_family = "unix")] - Self::Datagram(_) => Ok(()), - - #[cfg(target_family = "unix")] - Self::Stream(ref mut w) => w.flush(), - - Self::Udp(_) => Ok(()), - - Self::Tcp(ref mut w) => w.flush(), - } - } -} diff --git a/tests/test_syslog.rs b/tests/test_syslog.rs deleted file mode 100644 index 1937856..0000000 --- a/tests/test_syslog.rs +++ /dev/null @@ -1,75 +0,0 @@ -mod test_utils; - -#[cfg(feature = "syslog_writer")] -mod test { - - use flexi_logger::writers::{Syslog, SyslogFacility, SyslogWriter}; - use flexi_logger::{detailed_format, FileSpec, Logger}; - use log::*; - - #[macro_use] - mod macros { - #[macro_export] - macro_rules! syslog_error { - ($($arg:tt)*) => ( - error!(target: "{Syslog,_Default}", $($arg)*); - ) - } - } - - #[test] - fn test_syslog() -> std::io::Result<()> { - let boxed_syslog_writer = SyslogWriter::try_new( - SyslogFacility::LocalUse0, - None, - log::LevelFilter::Trace, - "JustForTest".to_owned(), - // Syslog::try_tcp("localhost:601")?, - Syslog::try_udp("127.0.0.1:5555", "127.0.0.1:514")?, - ) - .unwrap(); - let logger = Logger::try_with_str("info") - .unwrap() - .format(detailed_format) - .log_to_file( - FileSpec::default() - .suppress_timestamp() - .directory(super::test_utils::dir()), - ) - .print_message() - .add_writer("Syslog", boxed_syslog_writer) - .start() - .unwrap_or_else(|e| panic!("Logger initialization failed with {e}")); - - // Explicitly send logs to different loggers - error!(target : "{Syslog}", "This is a syslog-relevant error message"); - warn!(target : "{Syslog}", "This is a syslog-relevant error message"); - info!(target : "{Syslog}", "This is a syslog-relevant error message"); - debug!(target : "{Syslog}", "This is a syslog-relevant error message"); - trace!(target : "{Syslog}", "This is a syslog-relevant error message"); - - error!(target : "{Syslog,_Default}", "This is a syslog- and log-relevant error message"); - - // Nicer: use explicit macros - syslog_error!("This is another syslog- and log-relevant error message"); - warn!("This is a warning message"); - debug!("This is a debug message - you must not see it!"); - trace!("This is a trace message - you must not see it!"); - - // Verification: - logger.validate_logs(&[ - ( - "ERROR", - "syslog", - "a syslog- and log-relevant error message", - ), - ( - "ERROR", - "syslog", - "another syslog- and log-relevant error message", - ), - ("WARN", "syslog", "This is a warning message"), - ]); - Ok(()) - } -}