fastly/
log.rs

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