log-fastly 0.9.1

Implementation of the `log` façade for Fastly Compute@Edge
Documentation
//! Stub implementations of `Endpoint` and test helpers for testing without Wasm.
//!
//! This module is only built if `cfg(feature="native-test-stubs")`, and should only be used for
//! testing.

#![allow(missing_docs)]

pub use anyhow::{anyhow, Error};

use super::Logger;
use lazy_static::lazy_static;
use std::sync::{Arc, Mutex, MutexGuard, PoisonError};

lazy_static! {
    /// `LoggerGuard` holds this lock to prevent simultaneous tests from clobbering the global `log`
    /// state.
    ///
    /// Note that we do not care if this mutex gets poisoned, because that happens naturally if one
    /// of the tests fails an assertion.
    static ref GLOBAL_MUTEX: Mutex<()> = Mutex::new(());
    /// This holds the `Logger` that our stub implementation delegates to.
    static ref LOGGER: Arc<Mutex<Option<Logger>>> = Arc::new(Mutex::new(None));
    /// This is the last message seen by the current logger, used to check assertions.
    static ref LAST_MESSAGE: Mutex<(String, String)> = Mutex::new(("".to_owned(), "".to_owned()));
}

/// A stand-in for `fastly::log::Endpoint` used for testing.
///
/// We use this in place of the real `Endpoint` type in order to implement the logging behavior
/// natively, rather than via hostcalls.
#[derive(Clone, Debug)]
pub struct Endpoint {
    name: String,
}

impl Endpoint {
    pub fn name(&self) -> &str {
        self.name.as_str()
    }
}

impl TryFrom<&str> for Endpoint {
    type Error = Error;

    fn try_from(name: &str) -> Result<Self, Self::Error> {
        Ok(Self {
            name: name.to_owned(),
        })
    }
}

impl TryFrom<String> for Endpoint {
    type Error = Error;

    fn try_from(name: String) -> Result<Self, Self::Error> {
        Ok(Self { name })
    }
}

impl std::io::Write for Endpoint {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let msg = String::from_utf8_lossy(buf).to_string();

        *LAST_MESSAGE.lock().unwrap() = (self.name.clone(), msg);

        Ok(buf.len())
    }

    fn flush(&mut self) -> std::io::Result<()> {
        Ok(())
    }
}

pub struct LoggerGuard {
    _guard: MutexGuard<'static, ()>,
}

impl LoggerGuard {
    /// Assert that the most recent logged message was to the given endpoint, and contains the given
    /// string.
    pub fn assert_contains(&self, endpoint: &str, needle: &str) {
        let last = LAST_MESSAGE.lock().unwrap();
        let (last_endpoint, last_msg) = (last.0.to_owned(), last.1.to_owned());
        drop(last);
        assert!(
            endpoint == last_endpoint.as_str() && last_msg.contains(needle),
            "looking for {:?}: {:?}; found {:?}: {:?}",
            endpoint,
            needle,
            last_endpoint,
            last_msg
        );
    }

    /// Assert that the most recent logged message was either not to the given endpoint, or doesn't
    /// contain the given string.
    pub fn assert_absent(&self, endpoint: &str, poison: &str) {
        let last = LAST_MESSAGE.lock().unwrap();
        let (last_endpoint, last_msg) = (last.0.to_owned(), last.1.to_owned());
        drop(last);
        assert!(
            endpoint != last_endpoint.as_str() || !last_msg.contains(poison),
            "avoiding {:?}: {:?}; found {:?}: {:?}",
            endpoint,
            poison,
            last_endpoint,
            last_msg
        );
    }
}

impl Drop for LoggerGuard {
    fn drop(&mut self) {
        *LOGGER.lock().unwrap_or_else(PoisonError::into_inner) = None;
    }
}

/// Install a new logger, reset the last seen message, and return a lock on the global logger state.
///
/// This should be called in tests in place of `log::set_logger()` or `Builder::init()`.
pub fn reset_logger(logger: Logger) -> LoggerGuard {
    let guard = GLOBAL_MUTEX.lock().unwrap_or_else(PoisonError::into_inner);
    *LAST_MESSAGE.lock().unwrap() = ("".to_owned(), "".to_owned());
    log::set_max_level(logger.max_level());
    *LOGGER.lock().unwrap() = Some(logger);
    let _ = log::set_logger(&GlobalLogger);
    LoggerGuard { _guard: guard }
}

/// A `Log` impl that delegates to the global `Logger`, if it's set.
struct GlobalLogger;

impl log::Log for GlobalLogger {
    fn enabled(&self, metadata: &log::Metadata) -> bool {
        if let Some(logger) = LOGGER.lock().unwrap().as_ref() {
            logger.enabled(metadata)
        } else {
            false
        }
    }

    fn log(&self, record: &log::Record) {
        if let Some(logger) = LOGGER.lock().unwrap().as_ref() {
            logger.log(record)
        }
    }

    fn flush(&self) {}
}