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}