1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
//! Low-level interface to Fastly's [Real-Time Log Streaming][about] endpoints.
//!
//! Most applications should use the high-level interface provided by
//! [`log-fastly`](https://docs.rs/log-fastly), which includes management of log levels and easier
//! formatting.
//!
//! To write to an [`Endpoint`], you can use any interface that works with [`std::io::Write`],
//! including [`write!()`] and [`writeln!()`].
//!
//! 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
use crate::abi;
use fastly_shared::FastlyStatus;
use std::io::Write;
use thiserror::Error;
/// A Fastly logging endpoint.
///
/// Most applications should use the high-level interface provided by
/// [`log-fastly`](https://docs.rs/log-fastly) rather than writing to this interface directly.
///
/// To write to this endpoint, use the [`std::io::Write`] 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();
/// ```
#[derive(Clone, 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,
/// The endpoint name is too large.
#[error("endpoint name is too large")]
NameTooLarge,
}
impl TryFrom<&str> for Endpoint {
type Error = LogError;
fn try_from(name: &str) -> Result<Self, Self::Error> {
Self::try_from_name(name)
}
}
impl 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::fastly_log::write(self.handle(), buf.as_ptr(), buf.len(), &mut nwritten)
};
match status {
FastlyStatus::OK => Ok(nwritten),
FastlyStatus::BADF => Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"fastly_log::write failed: invalid log endpoint handle",
)),
FastlyStatus::BUFLEN => Err(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"fastly_log::write failed: log line too long",
)),
_ => Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("fastly_log::write failed: {:?}", status),
)),
}
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
impl Endpoint {
pub(crate) unsafe fn handle(&self) -> u32 {
self.handle
}
/// Get the name of an `Endpoint`.
pub fn name(&self) -> &str {
self.name.as_str()
}
/// 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 be empty
///
/// - 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::fastly_log::endpoint_get(name.as_ptr(), name.len(), &mut handle) };
match status {
FastlyStatus::OK => Ok(Endpoint {
handle,
name: name.to_owned(),
}),
FastlyStatus::INVAL => Err(LogError::InvalidEndpoint),
FastlyStatus::LIMITEXCEEDED => Err(LogError::NameTooLarge),
_ => panic!("fastly_log::endpoint_get failed"),
}
}
}
fn validate_endpoint_name(name: &str) -> Result<(), LogError> {
if name.is_empty() || name.find(|c| c == '\n' || c == ':').is_some() {
Err(LogError::MalformedEndpointName)
} else {
Ok(())
}
}
/// Set the logging endpoint where the message from Rust panics will be written.
///
/// By default, panic output is written to the `stderr` endpoint. Calling this function will
/// override that default with the endpoint, which may be provided as a string or an
/// [`Endpoint`].
///
/// ```no_run
/// fastly::log::set_panic_endpoint("my_error_endpoint").unwrap();
/// panic!("oh no!");
/// // will log "panicked at 'oh no', your/file.rs:line:col" to "my_error_endpoint"
/// ```
pub fn set_panic_endpoint<E>(endpoint: E) -> Result<(), LogError>
where
E: TryInto<Endpoint, Error = LogError>,
{
let endpoint = endpoint.try_into()?;
std::panic::set_hook(Box::new(move |info| {
// explicitly buffer this with `to_string()` to avoid multiple `write` calls
write!(endpoint.clone(), "{}", info.to_string()).expect("write succeeds in panic hook");
}));
Ok(())
}