logforth-append-syslog 0.3.0

Syslog appender for Logforth.
Documentation
// Copyright 2024 FastLabs Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Appender for writing log records to syslog.
//!
//! # Examples
//!
//!```rust, no_run
//! use logforth_append_syslog::Syslog;
//! use logforth_append_syslog::SyslogBuilder;
//! use logforth_core::record::LevelFilter;
//!
//! let append = SyslogBuilder::tcp_well_known().unwrap().build();
//!
//! logforth_core::builder()
//!     .dispatch(|d| d.filter(LevelFilter::All).append(append))
//!     .apply();
//!
//! log::info!("This log will be written to syslog.");
//! ```

#![cfg_attr(docsrs, feature(doc_cfg))]

use std::io;
use std::sync::Mutex;
use std::sync::MutexGuard;

use fasyslog::SDElement;
use fasyslog::format::SyslogContext;
use fasyslog::sender::SyslogSender;
use logforth_core::Append;
use logforth_core::Diagnostic;
use logforth_core::Error;
use logforth_core::Layout;
use logforth_core::record::Level;
use logforth_core::record::Record;

pub extern crate fasyslog;

/// The format of the syslog message.
#[derive(Debug, Copy, Clone)]
pub enum SyslogFormat {
    /// [RFC 3614] (BSD syslog Protocol)
    ///
    /// [RFC 3164]: https://datatracker.ietf.org/doc/html/rfc3164
    RFC3164,
    /// [RFC 5424] (The Syslog Protocol)
    ///
    /// [RFC 5424]: https://datatracker.ietf.org/doc/html/rfc5424
    RFC5424,
}

/// A builder to configure and create an [`Syslog`] appender.
#[derive(Debug)]
pub struct SyslogBuilder {
    sender: SyslogSender,
    formatter: SyslogFormatter,
}

impl SyslogBuilder {
    /// Create a new builder.
    pub fn new(sender: SyslogSender) -> Self {
        Self {
            sender,
            formatter: SyslogFormatter {
                format: SyslogFormat::RFC3164,
                context: SyslogContext::default(),
                layout: None,
            },
        }
    }

    /// Build the [`Syslog`] appender.
    pub fn build(self) -> Syslog {
        let SyslogBuilder { sender, formatter } = self;
        Syslog::new(sender, formatter)
    }

    /// Set the format of the [`Syslog`] appender.
    pub fn format(mut self, format: SyslogFormat) -> Self {
        self.formatter.format = format;
        self
    }

    /// Set the context of the [`Syslog`] appender.
    pub fn context(mut self, context: SyslogContext) -> Self {
        self.formatter.context = context;
        self
    }

    /// Set the layout of the [`Syslog`] appender.
    ///
    /// Default to `None`, the message will construct with only [`Record::payload`].
    pub fn layout(mut self, layout: impl Into<Box<dyn Layout>>) -> Self {
        self.formatter.layout = Some(layout.into());
        self
    }

    /// Create a new syslog writer that sends messages to the well-known TCP port (514).
    pub fn tcp_well_known() -> io::Result<SyslogBuilder> {
        fasyslog::sender::tcp_well_known()
            .map(SyslogSender::Tcp)
            .map(Self::new)
    }

    /// Create a new syslog writer that sends messages to the given TCP address.
    pub fn tcp<A: std::net::ToSocketAddrs>(addr: A) -> io::Result<SyslogBuilder> {
        fasyslog::sender::tcp(addr)
            .map(SyslogSender::Tcp)
            .map(Self::new)
    }

    /// Create a new syslog writer that sends messages to the well-known UDP port (514).
    pub fn udp_well_known() -> io::Result<SyslogBuilder> {
        fasyslog::sender::udp_well_known()
            .map(SyslogSender::Udp)
            .map(Self::new)
    }

    /// Create a new syslog writer that sends messages to the given UDP address.
    pub fn udp<L: std::net::ToSocketAddrs, R: std::net::ToSocketAddrs>(
        local: L,
        remote: R,
    ) -> io::Result<SyslogBuilder> {
        fasyslog::sender::udp(local, remote)
            .map(SyslogSender::Udp)
            .map(Self::new)
    }

    /// Create a new syslog writer that broadcast messages to the well-known UDP port (514).
    pub fn broadcast_well_known() -> io::Result<SyslogBuilder> {
        fasyslog::sender::broadcast_well_known()
            .map(SyslogSender::Udp)
            .map(Self::new)
    }

    /// Create a new syslog writer that broadcast messages to the given UDP address.
    pub fn broadcast(port: u16) -> io::Result<SyslogBuilder> {
        fasyslog::sender::broadcast(port)
            .map(SyslogSender::Udp)
            .map(Self::new)
    }
}

#[cfg(feature = "rustls")]
mod rustls_ext {
    use std::io;
    use std::net::ToSocketAddrs;
    use std::sync::Arc;

    use fasyslog::sender::SyslogSender;
    use fasyslog::sender::rustls::ClientConfig;

    use super::SyslogBuilder;

    impl SyslogBuilder {
        /// Create a TLS sender that sends messages to the well-known port (6514).
        pub fn rustls_well_known<S: Into<String>>(domain: S) -> io::Result<SyslogBuilder> {
            fasyslog::sender::rustls_well_known(domain)
                .map(Box::new)
                .map(SyslogSender::RustlsSender)
                .map(Self::new)
        }

        /// Create a TLS sender that sends messages to the given address.
        pub fn rustls<A: ToSocketAddrs, S: Into<String>>(
            addr: A,
            domain: S,
        ) -> io::Result<SyslogBuilder> {
            fasyslog::sender::rustls(addr, domain)
                .map(Box::new)
                .map(SyslogSender::RustlsSender)
                .map(Self::new)
        }

        /// Create a TLS sender that sends messages to the given address with certificate builder.
        pub fn rustls_with<A: ToSocketAddrs, S: Into<String>>(
            addr: A,
            domain: S,
            config: Arc<ClientConfig>,
        ) -> io::Result<SyslogBuilder> {
            fasyslog::sender::rustls_with(addr, domain, config)
                .map(Box::new)
                .map(SyslogSender::RustlsSender)
                .map(Self::new)
        }
    }
}

#[cfg(feature = "native-tls")]
mod native_tls_ext {
    use std::io;
    use std::net::ToSocketAddrs;

    use fasyslog::sender::SyslogSender;
    use fasyslog::sender::native_tls::TlsConnectorBuilder;

    use super::SyslogBuilder;

    impl SyslogBuilder {
        /// Create a TLS sender that sends messages to the well-known port (6514).
        pub fn native_tls_well_known<S: AsRef<str>>(domain: S) -> io::Result<SyslogBuilder> {
            fasyslog::sender::native_tls_well_known(domain)
                .map(SyslogSender::NativeTlsSender)
                .map(Self::new)
        }

        /// Create a TLS sender that sends messages to the given address.
        pub fn native_tls<A: ToSocketAddrs, S: AsRef<str>>(
            addr: A,
            domain: S,
        ) -> io::Result<SyslogBuilder> {
            fasyslog::sender::native_tls(addr, domain)
                .map(SyslogSender::NativeTlsSender)
                .map(Self::new)
        }

        /// Create a TLS sender that sends messages to the given address with certificate builder.
        pub fn native_tls_with<A: ToSocketAddrs, S: AsRef<str>>(
            addr: A,
            domain: S,
            builder: TlsConnectorBuilder,
        ) -> io::Result<SyslogBuilder> {
            fasyslog::sender::native_tls_with(addr, domain, builder)
                .map(SyslogSender::NativeTlsSender)
                .map(Self::new)
        }
    }
}

#[cfg(unix)]
mod unix_ext {
    use std::io;

    use fasyslog::sender::SyslogSender;

    use super::SyslogBuilder;

    impl SyslogBuilder {
        /// Create a new syslog writer that sends messages to the given Unix stream socket.
        pub fn unix_stream(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
            fasyslog::sender::unix_stream(path)
                .map(SyslogSender::UnixStream)
                .map(Self::new)
        }

        /// Create a new syslog writer that sends messages to the given Unix datagram socket.
        pub fn unix_datagram(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
            fasyslog::sender::unix_datagram(path)
                .map(SyslogSender::UnixDatagram)
                .map(Self::new)
        }

        /// Create a new syslog writer that sends messages to the given Unix socket.
        ///
        /// This method will automatically choose between `unix_stream` and `unix_datagram` based on
        /// the path.
        pub fn unix(path: impl AsRef<std::path::Path>) -> io::Result<SyslogBuilder> {
            fasyslog::sender::unix(path).map(Self::new)
        }
    }
}

/// An appender that writes log records to syslog.
#[derive(Debug)]
pub struct Syslog {
    sender: Mutex<SyslogSender>,
    formatter: SyslogFormatter,
}

impl Syslog {
    /// Creates a new [`Syslog`] appender.
    fn new(sender: SyslogSender, formatter: SyslogFormatter) -> Self {
        let sender = Mutex::new(sender);
        Self { sender, formatter }
    }

    fn sender(&self) -> MutexGuard<'_, SyslogSender> {
        self.sender.lock().unwrap_or_else(|e| e.into_inner())
    }
}

impl Append for Syslog {
    fn append(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<(), Error> {
        let message = self.formatter.format_message(record, diags)?;
        let mut sender = self.sender();
        sender
            .send_formatted(&message)
            .map_err(Error::from_io_error)?;
        Ok(())
    }

    fn flush(&self) -> Result<(), Error> {
        let mut sender = self.sender();
        sender.flush().map_err(Error::from_io_error)?;
        Ok(())
    }
}

impl Drop for Syslog {
    fn drop(&mut self) {
        let sender = self.sender.get_mut().unwrap_or_else(|e| e.into_inner());
        let _ = sender.flush();
    }
}

#[derive(Debug)]
struct SyslogFormatter {
    format: SyslogFormat,
    context: SyslogContext,
    layout: Option<Box<dyn Layout>>,
}

// @see https://opentelemetry.io/docs/specs/otel/logs/data-model-appendix/#appendix-b-severitynumber-example-mappings
fn log_level_to_syslog_severity(level: Level) -> fasyslog::Severity {
    match level {
        Level::Fatal | Level::Fatal2 | Level::Fatal3 | Level::Fatal4 => {
            fasyslog::Severity::EMERGENCY
        }
        Level::Error3 | Level::Error4 => fasyslog::Severity::ALERT,
        Level::Error2 => fasyslog::Severity::CRITICAL,
        Level::Error => fasyslog::Severity::ERROR,
        Level::Warn | Level::Warn2 | Level::Warn3 | Level::Warn4 => fasyslog::Severity::WARNING,
        Level::Info2 | Level::Info3 | Level::Info4 => fasyslog::Severity::NOTICE,
        Level::Info => fasyslog::Severity::INFORMATIONAL,
        Level::Debug
        | Level::Debug2
        | Level::Debug3
        | Level::Debug4
        | Level::Trace
        | Level::Trace2
        | Level::Trace3
        | Level::Trace4 => fasyslog::Severity::DEBUG,
    }
}

impl SyslogFormatter {
    fn format_message(
        &self,
        record: &Record,
        diags: &[Box<dyn Diagnostic>],
    ) -> Result<Vec<u8>, Error> {
        let severity = log_level_to_syslog_severity(record.level());

        let message = match self.format {
            SyslogFormat::RFC3164 => match self.layout {
                None => format!(
                    "{}",
                    self.context
                        .format_rfc3164(severity, Some(record.payload()))
                ),
                Some(ref layout) => {
                    let message = layout.format(record, diags)?;
                    let message = String::from_utf8_lossy(&message);
                    format!("{}", self.context.format_rfc3164(severity, Some(message)))
                }
            },
            SyslogFormat::RFC5424 => {
                const EMPTY_MSGID: Option<&str> = None;
                const EMPTY_STRUCTURED_DATA: Vec<SDElement> = Vec::new();

                match self.layout {
                    None => format!(
                        "{}",
                        self.context.format_rfc5424(
                            severity,
                            EMPTY_MSGID,
                            EMPTY_STRUCTURED_DATA,
                            Some(record.payload())
                        )
                    ),
                    Some(ref layout) => {
                        let message = layout.format(record, diags)?;
                        let message = String::from_utf8_lossy(&message);
                        format!(
                            "{}",
                            self.context.format_rfc5424(
                                severity,
                                EMPTY_MSGID,
                                EMPTY_STRUCTURED_DATA,
                                Some(message)
                            )
                        )
                    }
                }
            }
        };

        Ok(message.into_bytes())
    }
}