sift_error/
lib.rs

1use std::{error::Error as StdError, fmt, result::Result as StdResult};
2
3#[cfg(test)]
4mod test;
5
6/// Other Sift crates should just import this prelude to get everything necessary to construct
7/// [Error] types.
8pub mod prelude {
9    pub use super::{Error, ErrorKind, Result, SiftError};
10}
11
12/// A `Result` that returns [Error] as the error-type.
13pub type Result<T> = StdResult<T, Error>;
14pub type BoxedError = Box<dyn std::error::Error + Send + Sync>;
15
16/// Trait that defines the behavior of errors that Sift manages.
17pub trait SiftError<T, C>
18where
19    C: fmt::Display + Send + Sync + 'static,
20{
21    /// Adds context that is printed with the error.
22    fn context(self, ctx: C) -> Result<T>;
23
24    /// Like `context` but takes in a closure.
25    fn with_context<F>(self, op: F) -> Result<T>
26    where
27        F: Fn() -> C;
28
29    /// User-help text.
30    fn help(self, txt: C) -> Result<T>;
31}
32
33/// Error type returned across all Sift crates.
34#[derive(Debug)]
35pub struct Error {
36    context: Option<Vec<String>>,
37    help: Option<String>,
38    kind: ErrorKind,
39    inner: Option<BoxedError>,
40}
41
42impl StdError for Error {}
43
44impl Error {
45    /// Initializes an [Error].
46    pub fn new<E>(kind: ErrorKind, err: E) -> Self
47    where
48        E: StdError + Send + Sync + 'static,
49    {
50        let inner = Box::new(err);
51        Self {
52            inner: Some(inner),
53            kind,
54            context: None,
55            help: None,
56        }
57    }
58
59    /// Initializes an [Error] with a generic message.
60    pub fn new_msg<S: AsRef<str>>(kind: ErrorKind, msg: S) -> Self {
61        Self {
62            inner: None,
63            kind,
64            context: Some(vec![msg.as_ref().to_string()]),
65            help: None,
66        }
67    }
68
69    /// Initializes a general catch-all type of [Error]. Contributors should be careful not to use
70    /// this unless strictly necessary.
71    pub fn new_general<S: AsRef<str>>(msg: S) -> Self {
72        Self::new_msg(ErrorKind::GeneralError, msg)
73    }
74
75    /// Used for user-errors that have to do with bad arguments.
76    pub fn new_arg_error<S: AsRef<str>>(msg: S) -> Self {
77        Self::new_msg(ErrorKind::ArgumentValidationError, msg)
78    }
79
80    /// Tonic response types usually return optional types that we need to handle; if responses are
81    /// empty then this is the appropriate way to initialize an [Error] for that situation, though
82    /// this has never been observed.
83    pub fn new_empty_response<S: AsRef<str>>(msg: S) -> Self {
84        Self {
85            inner: None,
86            kind: ErrorKind::EmptyResponseError,
87            context: Some(vec![msg.as_ref().to_string()]),
88            help: Some("please contact Sift".to_string()),
89        }
90    }
91
92    /// Get the underlying error kind.
93    pub fn kind(&self) -> ErrorKind {
94        self.kind
95    }
96}
97
98/// Various categories of errors that can occur throughout Sift crates.
99#[derive(Debug, PartialEq, Copy, Clone)]
100pub enum ErrorKind {
101    /// Indicates user-error having to do with bad arguments.
102    ArgumentValidationError,
103    /// Indicates that the program is unable to grab credentials from a user's `sift.toml` file.
104    ConfigError,
105    /// Inidicates that the program was unable to connect to Sift.
106    GrpcConnectError,
107    /// Indicates that the program was unable to retrieve the run being requested.
108    RetrieveRunError,
109    /// Indicates that the program was unable to retrieve the asset being requested.
110    RetrieveAssetError,
111    /// Indicates a failure to update a run.
112    UpdateRunError,
113    /// Indicates that the program was unable to retrieve the ingestion config being requested.
114    RetrieveIngestionConfigError,
115    /// Indicates a failure to create a run.
116    CreateRunError,
117    /// Indicates a failure to create an ingestion config.
118    CreateIngestionConfigError,
119    /// Indicates a failure to create a flow.
120    CreateFlowError,
121    /// Indicates a failure to find the requested resource, likely because it doesn't exist.
122    NotFoundError,
123    /// General I/O errors.
124    IoError,
125    /// Indicates that there was a conversion between numeric times.
126    NumberConversionError,
127    /// Indicates a failure to generated a particular time-type from arguments.
128    TimeConversionError,
129    /// General errors that can occur while streaming telemetry i.e. data ingestion.
130    StreamError,
131    /// Indicates that all retries were exhausted in the configure retry policy.
132    RetriesExhausted,
133    /// General errors that can occur while processing backups during streaming.
134    BackupsError,
135    /// Indicates that the user is making a change that is not backwards compatible with an
136    /// existing ingestion config.
137    IncompatibleIngestionConfigChange,
138    /// Indicates that a user provided a flow-name that doesn't match any configured flow in the
139    /// parent ingestion config.
140    UnknownFlow,
141    /// This really shouldn't happen.
142    EmptyResponseError,
143    /// When failing to decode protobuf from its wire format.
144    ProtobufDecodeError,
145    /// When backup checksums don't match.
146    BackupIntegrityError,
147    /// When backup file/buffer limit has been reached.
148    BackupLimitReached,
149    /// General errors that are rarely returned.
150    GeneralError,
151}
152
153impl<T, C> SiftError<T, C> for Result<T>
154where
155    C: fmt::Display + Send + Sync + 'static,
156{
157    fn with_context<F>(self, op: F) -> Result<T>
158    where
159        F: Fn() -> C,
160    {
161        self.map_err(|mut err| {
162            if let Some(context) = err.context.as_mut() {
163                context.push(format!("{}", op()));
164            } else {
165                err.context = Some(vec![format!("{}", op())]);
166            }
167            err
168        })
169    }
170
171    fn context(self, ctx: C) -> Self {
172        self.map_err(|mut err| {
173            if let Some(context) = err.context.as_mut() {
174                context.push(format!("{ctx}"));
175            } else {
176                err.context = Some(vec![format!("{ctx}")]);
177            }
178            err
179        })
180    }
181
182    fn help(self, txt: C) -> Self {
183        self.map_err(|mut err| {
184            err.help = Some(format!("{txt}"));
185            err
186        })
187    }
188}
189
190impl fmt::Display for ErrorKind {
191    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
192        match self {
193            Self::GrpcConnectError => write!(f, "GrpcConnectError"),
194            Self::RetriesExhausted => write!(f, "RetriesExhausted"),
195            Self::RetrieveAssetError => write!(f, "RetrieveAssetError"),
196            Self::RetrieveRunError => write!(f, "RetrieveRunError"),
197            Self::RetrieveIngestionConfigError => write!(f, "RetrieveIngestionConfigError"),
198            Self::EmptyResponseError => write!(f, "EmptyResponseError"),
199            Self::NotFoundError => write!(f, "NotFoundError"),
200            Self::CreateRunError => write!(f, "CreateRunError"),
201            Self::ArgumentValidationError => write!(f, "ArgumentValidationError"),
202            Self::GeneralError => write!(f, "GeneralError"),
203            Self::IoError => write!(f, "IoError"),
204            Self::ConfigError => write!(f, "ConfigError"),
205            Self::UpdateRunError => write!(f, "UpdateRunError"),
206            Self::CreateIngestionConfigError => write!(f, "CreateIngestionConfigError"),
207            Self::NumberConversionError => write!(f, "NumberConversionError"),
208            Self::CreateFlowError => write!(f, "CreateFlowError"),
209            Self::TimeConversionError => write!(f, "TimeConversionError"),
210            Self::StreamError => write!(f, "StreamError"),
211            Self::UnknownFlow => write!(f, "UnknownFlow"),
212            Self::BackupsError => write!(f, "BackupsError"),
213            Self::BackupIntegrityError => write!(f, "BackupIntegrityError"),
214            Self::BackupLimitReached => write!(f, "BackupLimitReached"),
215            Self::ProtobufDecodeError => write!(f, "ProtobufDecodeError"),
216            Self::IncompatibleIngestionConfigChange => {
217                write!(f, "IncompatibleIngestionConfigChange")
218            }
219        }
220    }
221}
222
223const NEW_LINE_DELIMITER: &str = "\n   ";
224
225impl fmt::Display for Error {
226    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227        let Error {
228            context,
229            kind,
230            help,
231            inner,
232        } = self;
233
234        let root_cause = inner.as_ref().map(|e| format!("{e}"));
235
236        let (most_recent_cause, chain) = context.as_ref().map_or_else(
237            || {
238                let root = root_cause.clone().unwrap_or_default();
239                (String::new(), format!("- {root}"))
240            },
241            |c| {
242                let mut cause_iter = c.iter().rev();
243
244                if let Some(first) = cause_iter.next() {
245                    let mut cause_chain = cause_iter
246                        .map(|s| format!("- {s}"))
247                        .collect::<Vec<String>>()
248                        .join(NEW_LINE_DELIMITER);
249
250                    if let Some(root) = root_cause.clone() {
251                        if cause_chain.is_empty() {
252                            cause_chain = format!("- {root}");
253                        } else {
254                            cause_chain = format!("{cause_chain}{NEW_LINE_DELIMITER}- {root}");
255                        }
256                    }
257
258                    (first.clone(), cause_chain)
259                } else {
260                    (
261                        String::new(),
262                        root_cause
263                            .as_ref()
264                            .map_or_else(String::new, |s| format!("- {s}")),
265                    )
266                }
267            },
268        );
269
270        match help {
271            Some(help_txt) if most_recent_cause.is_empty() => {
272                writeln!(
273                    f,
274                    "[{kind}]\n\n[cause]:{NEW_LINE_DELIMITER}{chain}\n\n[help]:{NEW_LINE_DELIMITER}- {help_txt}"
275                )
276            }
277            None if most_recent_cause.is_empty() => {
278                writeln!(f, "[{kind}]\n\n[cause]:{NEW_LINE_DELIMITER}{chain}")
279            }
280            Some(help_txt) => {
281                writeln!(
282                    f,
283                    "[{kind}]: {most_recent_cause}\n\n[cause]:{NEW_LINE_DELIMITER}{chain}\n\n[help]:{NEW_LINE_DELIMITER}- {help_txt}"
284                )
285            }
286            None => {
287                writeln!(
288                    f,
289                    "[{kind}]: {most_recent_cause}\n\n[cause]:{NEW_LINE_DELIMITER}{chain}"
290                )
291            }
292        }
293    }
294}
295
296impl From<std::io::Error> for Error {
297    fn from(value: std::io::Error) -> Self {
298        Self {
299            context: None,
300            help: None,
301            inner: Some(Box::new(value)),
302            kind: ErrorKind::IoError,
303        }
304    }
305}