fastly 0.2.0-alpha4

Fastly Compute@Edge API
Documentation
//! Interface to Fastly's [Real-Time Log Streaming][about] endpoints.
//!
//! To write to an `Endpoint`, you can use any interface that works with
//! [`std::io::Write`][std-write-trait], including [`write!()`][std-write-macro] and
//! [`writeln!()`][std-writeln-macro].
//!
//! Each write to the endpoint emits a single log line, so any newlines that are present in the
//! message are escaped to the character sequence `"\n"`.
//!
//! [about]: https://docs.fastly.com/en/guides/about-fastlys-realtime-log-streaming-features
//! [std-write-trait]: https://doc.rust-lang.org/std/io/trait.Write.html
//! [std-write-macro]: https://doc.rust-lang.org/std/macro.write.html
//! [std-writeln-macro]: https://doc.rust-lang.org/std/macro.writeln.html
// TODO ACF 2020-04-03: uncomment when this becomes true!
// //! This interface can be used directly, but also serves as the basis for more feature-rich logging
// //! through integration with [`log`][log] and [`tracing`][tracing].

use crate::abi;
use fastly_shared::XqdStatus;
use lazy_static::lazy_static;
use regex::Regex;
use thiserror::Error;

/// A Fastly logging endpoint.
///
/// To write to this endpoint, use the [`std::io::Write`][std-write-trait] interface. For example:
///
/// ```no_run
/// # use fastly::log::Endpoint;
/// # use std::io::Write;
/// let mut endpoint = Endpoint::from_name("my_endpoint");
/// writeln!(endpoint, "Hello from the edge!").unwrap();
/// ```
///
/// [std-write-trait]: https://doc.rust-lang.org/std/io/trait.Write.html
#[derive(Eq, Hash, PartialEq)]
pub struct Endpoint {
    handle: u32,
    name: String,
}

// use a custom debug formatter to avoid the noise from the handle
impl std::fmt::Debug for Endpoint {
    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        fmt.debug_struct("Endpoint")
            .field("name", &self.name)
            .finish()
    }
}

/// Logging-related errors.
#[derive(Copy, Clone, Debug, Error, PartialEq, Eq)]
pub enum LogError {
    /// The endpoint could not be found, or is a reserved name.
    #[error("endpoint not found, or is reserved")]
    InvalidEndpoint,
    /// The endpoint name is malformed.
    #[error("malformed endpoint name")]
    MalformedEndpointName,
}

impl std::convert::TryFrom<&str> for Endpoint {
    type Error = LogError;

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

impl std::convert::TryFrom<String> for Endpoint {
    type Error = LogError;

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

impl std::io::Write for Endpoint {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        let mut nwritten = 0;
        let status =
            unsafe { abi::xqd_log_write(self.handle(), buf.as_ptr(), buf.len(), &mut nwritten) };
        match status {
            XqdStatus::OK => Ok(nwritten),
            XqdStatus::BADF => Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                "xqd_log_write failed: invalid log endpoint handle",
            )),
            XqdStatus::BUFLEN => Err(std::io::Error::new(
                std::io::ErrorKind::InvalidData,
                "xqd_log_write failed: log line too long",
            )),
            _ => Err(std::io::Error::new(
                std::io::ErrorKind::Other,
                format!("xqd_log_write failed: {:?}", status),
            )),
        }
    }

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

impl Endpoint {
    pub(crate) unsafe fn handle(&self) -> u32 {
        self.handle
    }

    /// Get an `Endpoint` by name.
    ///
    /// # Panics
    ///
    /// If the endpoint name is not valid, this function will panic.
    pub fn from_name(name: &str) -> Self {
        Self::try_from_name(name).unwrap()
    }

    /// Try to get an `Endpoint` by name.
    ///
    /// Currently, the conditions on an endpoint name are:
    ///
    /// - It must not contain newlines (`\n`) or colons (`:`)
    ///
    /// - It must not be `stdout` or `stderr`, which are reserved for debugging.
    pub fn try_from_name(name: &str) -> Result<Self, LogError> {
        validate_endpoint_name(name)?;
        let mut handle = 0u32;
        let status = unsafe { abi::xqd_log_endpoint_get(name.as_ptr(), name.len(), &mut handle) };
        match status {
            XqdStatus::OK => Ok(Endpoint {
                handle,
                name: name.to_owned(),
            }),
            XqdStatus::INVAL => Err(LogError::InvalidEndpoint),
            _ => panic!("xqd_log_endpoint_get failed"),
        }
    }
}

fn validate_endpoint_name(name: &str) -> Result<(), LogError> {
    lazy_static! {
        static ref VALID_ENDPOINT_RE: Regex = Regex::new(r"^[^\n:]*$").unwrap();
    }
    match name {
        "" => Err(LogError::MalformedEndpointName),
        name if !VALID_ENDPOINT_RE.is_match(name) => Err(LogError::MalformedEndpointName),
        _ => Ok(()),
    }
}