jeset 1.0.2

Opinionated, ready to use color-eyre and tracing-journald setup for applications
Documentation
//! Opinionated, ready to use color-eyre and tracing-journald setup for applications.

pub use color_eyre;
pub use color_eyre::eyre::{self, Report};

use tracing::{debug, error};
use tracing_error::ErrorLayer;
use tracing_subscriber::prelude::*;
use tracing_subscriber::{filter::LevelFilter, EnvFilter};

/// Systemd journal
/// [SYSLOG_IDENTIFIER](https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html#SYSLOG_FACILITY=)
/// variable.
const SYSLOG_IDENTIFIER: &str = "SYSLOG_IDENTIFIER";

/// Default maximum log verbosity level: INFO.
/// [Rationale](https://blog.datalust.co/choosing-the-right-log-levels/).
const DEFAULT_LEVEL: LevelFilter = LevelFilter::INFO;

/// Setup eyre (with `color_eyre`).
pub fn setup_eyre() -> Result<(), Report> {
    color_eyre::install()?;
    Ok(())
}

/// Setup tracing. Better to be called after `setup_eyre()?;`
///
/// Logs to stderr, or to journal if SYSLOG_IDENTIFIER environment variable is set and not empty.
///
/// Tracing verbosity level configuration is done setting the environment variable `RUST_LOG`,
/// with the argument of the function used as fallback.
/// The syntax to be used is detailed
/// [here](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives).
///
/// For example:
///
/// - `warn,tokio::net=info` will enable all spans and events that:
///     + are at the level warn or above, or
///     + have the tokio::net target at the level info or above.
/// - `""` is equivalent to the default `info`
/// - `off` will disable all logs
/// - the directives are case insensitive, `WARN` is equivalent to `warn`
///
/// If an invalid directive is set into `RUST_LOG` environment variable the fallback will be
/// silently used.
pub fn setup_tracing_with<D: AsRef<str>>(
    fallback_tracing_filter_directives: D,
) -> Result<(), Report> {
    setup_tracing(fallback_tracing_filter_directives)?;
    Ok(())
}

/// Shortcut for `setup_with("")` or `setup_with("info")`.
pub fn setup_tracing_default() -> Result<(), Report> {
    setup_tracing_with("")
}

fn setup_tracing<D: AsRef<str>>(fallback_tracing_filter_directives: D) -> Result<(), Report> {
    let fallback = fallback_envfilter(fallback_tracing_filter_directives.as_ref())?;
    match is_running_as_service() {
        false => install_stderr_tracing(fallback),
        true => install_journald_tracing(fallback).unwrap_or_else(|_| {
            let fallback = fallback_envfilter(fallback_tracing_filter_directives.as_ref()).unwrap();
            install_stderr_tracing(fallback);
            error!(
                "{} is set, but failed to connect to journald. Falling back to logging to stderr",
                SYSLOG_IDENTIFIER
            );
        }),
    };
    Ok(())
}

/// Return true if SYSLOG_IDENTIFIER environment variable is set.
fn is_running_as_service() -> bool {
    !std::env::var(SYSLOG_IDENTIFIER)
        .unwrap_or_default()
        .is_empty()
}

fn install_stderr_tracing(fallback_filter: EnvFilter) {
    let filter_layer = get_envfilter(fallback_filter);
    let filter_layer_repr = format!("{:?}", &filter_layer);
    let fmt_layer = tracing_subscriber::fmt::layer()
        .with_target(false)
        .with_writer(std::io::stderr);
    tracing_subscriber::registry()
        .with(filter_layer)
        .with(ErrorLayer::default())
        .with(fmt_layer)
        .init();
    debug!("Logging to stderr with {:?}", filter_layer_repr);
}

fn install_journald_tracing(fallback_filter: EnvFilter) -> Result<(), Report> {
    let filter_layer = get_envfilter(fallback_filter);
    let filter_layer_repr = format!("{:?}", &filter_layer);
    let fmt_layer = tracing_journald::layer()?;
    tracing_subscriber::registry()
        .with(filter_layer)
        .with(ErrorLayer::default())
        .with(fmt_layer)
        .init();
    debug!("Logging to journal with {:?}", filter_layer_repr);
    Ok(())
}

fn fallback_envfilter<D: AsRef<str>>(
    directives: D, // "" => DEFAULT_LEVEL
) -> Result<EnvFilter, tracing_subscriber::filter::ParseError> {
    EnvFilter::builder()
        .with_default_directive(DEFAULT_LEVEL.into())
        .parse(directives)
}

fn get_envfilter(fallback: EnvFilter) -> EnvFilter {
    EnvFilter::builder()
        .with_default_directive(DEFAULT_LEVEL.into())
        .try_from_env()
        .unwrap_or(fallback)
}

#[cfg(test)]
mod test {
    use super::*;
    use insta::assert_debug_snapshot;
    use serial_test::serial;
    // use tracing_test::traced_test;

    #[test]
    fn fallback_envfilter_good() {
        assert_debug_snapshot!(fallback_envfilter("info,sqlx=warn").unwrap());
    }

    #[test]
    fn fallback_envfilter_case() {
        assert_debug_snapshot!(fallback_envfilter("WarN").unwrap());
    }

    #[test]
    fn fallback_envfilter_empty() {
        assert_debug_snapshot!(fallback_envfilter("").unwrap());
    }

    #[test]
    fn fallback_envfilter_bad() {
        assert!(fallback_envfilter("=x").is_err());
    }

    #[test]
    #[serial]
    fn is_running_as_service_test() {
        // assuming SYSLOG_IDENTIFIER unset
        assert!(!is_running_as_service());
    }

    // TODO how do we drop the tracing subscriber at the end of each test?
    // #[test]
    // #[serial]
    // fn setup_with_empty() {
    //     assert!(!is_running_as_service());
    //     setup_tracing("").unwrap();
    //     assert_eq!(LevelFilter::current(), DEFAULT_LEVEL);
    // }
    //
    // #[test]
    // #[serial]
    // fn setup_off() {
    //     assert!(!is_running_as_service());
    //     setup_tracing("off").unwrap();
    //     assert_eq!(LevelFilter::current(), LevelFilter::OFF);
    // }
    //
    // #[test]
    // #[serial]
    // fn setup_lvl() {
    //     assert!(!is_running_as_service());
    //     setup_tracing("debug").unwrap();
    //     assert_eq!(LevelFilter::current(), LevelFilter::DEBUG);
    // }
    //
    // #[test]
    // #[serial]
    // fn setup_env() {
    //     assert!(!is_running_as_service());
    //     std::env::set_var("RUST_LOG", "trace");
    //     setup_tracing("info").unwrap();
    //     assert_eq!(LevelFilter::current(), LevelFilter::TRACE);
    // }

    #[test]
    #[serial]
    fn setup_default_env() {
        assert!(!is_running_as_service());
        std::env::set_var("RUST_LOG", "warn");
        setup_tracing("").unwrap();
        assert_eq!(LevelFilter::current(), LevelFilter::WARN);
    }

    // TODO how do we drop the tracing subscriber at the end of each test?
    // #[test]
    // #[serial]
    // fn setup_default_env_eyre() {
    //     assert!(!is_running_as_service());
    //     std::env::set_var("RUST_LOG", "warn");
    //     setup_tracing_default().unwrap();
    //     assert_eq!(LevelFilter::current(), LevelFilter::WARN);
    // }

    // TODO how do we drop the tracing subscriber at the end of each test?
    // #[test]
    // #[serial]
    // fn setup_default_test() {
    //     assert!(!is_running_as_service());
    //     setup_tracing_default().unwrap();
    //     assert_eq!(LevelFilter::current(), DEFAULT_LEVEL);
    // }

    #[test]
    #[serial]
    fn setup_eyre_test() {
        setup_eyre().unwrap();
    }
}