syslog-too 0.1.1

A small library to send log messages to syslog locally and over TCP or UDP using Rust. A continuation of the syslog crate.
Documentation
//! Syslog
//!
//! This crate provides facilities to send log messages via syslog.
//! It supports Unix sockets for local syslog, UDP and TCP for remote servers.
//!
//! Messages can be passed directly without modification, or in RFC 3164 or RFC 5424 format
//!
//! The code is available on [GitLab](https://gitlab.com/ngreese/syslog-too)
//!
//! # Example
//!
//! ```rust
//! use syslog_too::{Facility, Formatter3164};
//!
//! let formatter = Formatter3164 {
//!     facility: Facility::LOG_USER,
//!     hostname: None,
//!     process: "myprogram".into(),
//!     pid: 0,
//! };
//!
//! match syslog::unix(formatter) {
//!     Err(e) => println!("impossible to connect to syslog: {:?}", e),
//!     Ok(mut writer) => {
//!         writer.err("hello world").expect("could not write error message");
//!     }
//! }
//! ```
//!
//! It can be used directly with the log crate as follows:
//!
//! ```rust
//! extern crate log;
//!
//! use syslog_too::{Facility, Formatter3164, Logger3164};
//! use log::{SetLoggerError, LevelFilter, info};
//!
//! let formatter = Formatter3164 {
//!     facility: Facility::LOG_USER,
//!     hostname: None,
//!     process: "myprogram".into(),
//!     pid: 0,
//! };
//!
//! let logger = match syslog_too::unix(formatter) {
//!     Err(e) => { println!("impossible to connect to syslog: {:?}", e); return; },
//!     Ok(logger) => logger,
//! };
//! log::set_boxed_logger(Box::new(Logger3164::new(logger)))
//!         .map(|()| log::set_max_level(LevelFilter::Info));
//!
//! info!("hello world");
//! ```
extern crate log;
extern crate time;

use std::env;
#[cfg(not(unix))]
use std::io;
use std::io::{BufWriter, ErrorKind};
use std::net::{TcpStream, ToSocketAddrs, UdpSocket};
#[cfg(unix)]
use std::os::unix::net::{UnixDatagram, UnixStream};
use std::path::Path;
use std::process;

mod errors;
mod facility;
mod format;
mod logger;
#[cfg(test)]
mod tests;

pub use errors::*;
pub use facility::Facility;
pub use format::Severity;
pub use format::{Formatter3164, Formatter5424, LogFormat};
pub use logger::{Logger, Logger3164, Logger5424, LoggerBackend};

pub type Priority = u8;

#[cfg(unix)]
const UNIX_SOCK_PATHS: [&str; 3] = ["/dev/log", "/var/run/syslog", "/var/run/log"];

/// Returns a Logger using unix socket to target local syslog ( using /dev/log or /var/run/syslog)
#[cfg(unix)]
pub fn unix<F: Clone>(formatter: F) -> Result<Logger<LoggerBackend, F>> {
    UNIX_SOCK_PATHS
        .iter()
        .find_map(|path| {
            unix_connect(formatter.clone(), *path).map_or_else(
                |e| {
                    if let Error::Io(ref io_err) = e {
                        if io_err.kind() == ErrorKind::NotFound {
                            None // not considered an error, try the next path
                        } else {
                            Some(Err(e))
                        }
                    } else {
                        Some(Err(e))
                    }
                },
                |logger| Some(Ok(logger)),
            )
        })
        .transpose()
        .map_err(|e| Error::Initialization(Box::new(e)))?
        .ok_or_else(|| Error::Initialization("unix socket paths not found".into()))
}

#[cfg(not(unix))]
pub fn unix<F: Clone>(_formatter: F) -> Result<Logger<LoggerBackend, F>> {
    Err(io::Error::from(ErrorKind::Unsupported))?
}

/// Returns a Logger using unix socket to target local syslog at user provided path
#[cfg(unix)]
pub fn unix_custom<P: AsRef<Path>, F>(formatter: F, path: P) -> Result<Logger<LoggerBackend, F>> {
    unix_connect(formatter, path).map_err(|e| Error::Initialization(Box::new(e)))
}

#[cfg(not(unix))]
pub fn unix_custom<P: AsRef<Path>, F>(_formatter: F, _path: P) -> Result<Logger<LoggerBackend, F>> {
    Err(io::Error::from(ErrorKind::Unsupported))?
}

#[cfg(unix)]
fn unix_connect<P: AsRef<Path>, F>(formatter: F, path: P) -> Result<Logger<LoggerBackend, F>> {
    let sock = UnixDatagram::unbound()?;
    match sock.connect(&path) {
        Ok(()) => Ok(Logger {
            formatter,
            backend: LoggerBackend::Unix(sock),
        }),
        Err(ref e) if e.raw_os_error() == Some(libc::EPROTOTYPE) => {
            let sock = UnixStream::connect(path)?;
            Ok(Logger {
                formatter,
                backend: LoggerBackend::UnixStream(BufWriter::new(sock)),
            })
        }
        Err(e) => Err(e.into()),
    }
}

/// returns a UDP logger connecting `local` and `server`
pub fn udp<T: ToSocketAddrs, U: ToSocketAddrs, F>(
    formatter: F,
    local: T,
    server: U,
) -> Result<Logger<LoggerBackend, F>> {
    server
        .to_socket_addrs()
        .map_err(|e| Error::Initialization(Box::new(e)))
        .and_then(|mut server_addr_opt| {
            server_addr_opt
                .next()
                .ok_or_else(|| Error::Initialization("no server address".into()))
        })
        .and_then(|server_addr| {
            UdpSocket::bind(local)
                .map_err(|e| Error::Initialization(Box::new(e)))
                .map(|socket| Logger {
                    formatter,
                    backend: LoggerBackend::Udp(socket, server_addr),
                })
        })
}

/// returns a TCP logger connecting `local` and `server`
pub fn tcp<T: ToSocketAddrs, F>(formatter: F, server: T) -> Result<Logger<LoggerBackend, F>> {
    TcpStream::connect(server)
        .map_err(|e| Error::Initialization(e.into()))
        .map(|socket| Logger {
            formatter,
            backend: LoggerBackend::Tcp(BufWriter::new(socket)),
        })
}

/// Unix socket Logger init function compatible with log crate
#[cfg(unix)]
pub fn init_unix(facility: Facility, log_level: log::LevelFilter) -> Result<()> {
    let (process, pid) = get_process_info()?;
    let formatter = Formatter3164 {
        facility,
        hostname: None,
        process,
        pid,
    };
    unix(formatter).and_then(|logger| {
        log::set_boxed_logger(Box::new(Logger3164::new(logger)))
            .map_err(|e| Error::Initialization(Box::new(e)))
    })?;

    log::set_max_level(log_level);
    Ok(())
}

#[cfg(not(unix))]
pub fn init_unix(_facility: Facility, _log_level: log::LevelFilter) -> Result<()> {
    Err(io::Error::from(ErrorKind::Unsupported))?
}

/// Unix socket Logger init function compatible with log crate and user provided socket path
#[cfg(unix)]
pub fn init_unix_custom<P: AsRef<Path>>(
    facility: Facility,
    log_level: log::LevelFilter,
    path: P,
) -> Result<()> {
    let (process, pid) = get_process_info()?;
    let formatter = Formatter3164 {
        facility,
        hostname: None,
        process,
        pid,
    };
    unix_custom(formatter, path).and_then(|logger| {
        log::set_boxed_logger(Box::new(Logger3164::new(logger)))
            .map_err(|e| Error::Initialization(Box::new(e)))
    })?;

    log::set_max_level(log_level);
    Ok(())
}

#[cfg(not(unix))]
pub fn init_unix_custom<P: AsRef<Path>>(
    _facility: Facility,
    _log_level: log::LevelFilter,
    _path: P,
) -> Result<()> {
    Err(io::Error::from(ErrorKind::Unsupported))?
}

/// UDP Logger init function compatible with log crate
pub fn init_udp<T: ToSocketAddrs>(
    local: T,
    server: T,
    hostname: String,
    facility: Facility,
    log_level: log::LevelFilter,
) -> Result<()> {
    let (process, pid) = get_process_info()?;
    let formatter = Formatter3164 {
        facility,
        hostname: Some(hostname),
        process,
        pid,
    };
    udp(formatter, local, server).and_then(|logger| {
        log::set_boxed_logger(Box::new(Logger3164::new(logger)))
            .map_err(|e| Error::Initialization(Box::new(e)))
    })?;

    log::set_max_level(log_level);
    Ok(())
}

/// TCP Logger init function compatible with log crate
pub fn init_tcp<T: ToSocketAddrs>(
    server: T,
    hostname: String,
    facility: Facility,
    log_level: log::LevelFilter,
) -> Result<()> {
    let (process, pid) = get_process_info()?;
    let formatter = Formatter3164 {
        facility,
        hostname: Some(hostname),
        process,
        pid,
    };

    tcp(formatter, server).and_then(|logger| {
        log::set_boxed_logger(Box::new(Logger3164::new(logger)))
            .map_err(|e| Error::Initialization(Box::new(e)))
    })?;

    log::set_max_level(log_level);
    Ok(())
}

/// Initializes logging subsystem for log crate
///
/// This tries to connect to syslog by following ways:
///
/// 1. Unix sockets /dev/log and /var/run/syslog (in this order)
/// 2. Tcp connection to 127.0.0.1:601
/// 3. Udp connection to 127.0.0.1:514
///
/// Note the last option usually (almost) never fails in this method. So
/// this method doesn't return error even if there is no syslog.
///
/// If `application_name` is `None` name is derived from executable name
pub fn init(
    facility: Facility,
    log_level: log::LevelFilter,
    application_name: Option<&str>,
) -> Result<()> {
    let (process_name, pid) = get_process_info()?;
    let process = application_name.map(From::from).unwrap_or(process_name);
    let mut formatter = Formatter3164 {
        facility,
        hostname: None,
        process,
        pid,
    };

    let backend = if let Ok(logger) = unix(formatter.clone()) {
        logger.backend
    } else {
        formatter.hostname = get_hostname().ok();
        if let Ok(tcp_stream) = TcpStream::connect(("127.0.0.1", 601)) {
            LoggerBackend::Tcp(BufWriter::new(tcp_stream))
        } else {
            let udp_addr = "127.0.0.1:514".parse().unwrap();
            let udp_stream = UdpSocket::bind(("127.0.0.1", 0))?;
            LoggerBackend::Udp(udp_stream, udp_addr)
        }
    };
    log::set_boxed_logger(Box::new(Logger3164::new(Logger { formatter, backend })))
        .map_err(|e| Error::Initialization(Box::new(e)))?;

    log::set_max_level(log_level);
    Ok(())
}

fn get_process_info() -> Result<(String, u32)> {
    env::current_exe()
        .map_err(|e| Error::Initialization(Box::new(e)))
        .and_then(|path| {
            path.file_name()
                .and_then(|os_name| os_name.to_str())
                .map(|name| name.to_string())
                .ok_or_else(|| Error::Initialization("process name not found".into()))
        })
        .map(|name| (name, process::id()))
}

fn get_hostname() -> Result<String> {
    Ok(hostname::get()?.to_string_lossy().to_string())
}