Skip to main content

rlg/
error.rs

1// error.rs
2// Copyright © 2024-2026 RustLogs (RLG). All rights reserved.
3// SPDX-License-Identifier: Apache-2.0
4// SPDX-License-Identifier: MIT
5
6use crate::config::ConfigError;
7use std::fmt;
8use std::io;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12/// Error variants for the RLG logging pipeline.
13pub enum RlgError {
14    #[error("I/O error: {0}")]
15    /// I/O error
16    IoError(#[from] io::Error),
17
18    #[error("Configuration error: {0}")]
19    /// Configuration error
20    ConfigError(#[from] ConfigError),
21
22    #[error("Log format parse error: {0}")]
23    /// Log format parse error
24    FormatParseError(String),
25
26    #[error("Log level parse error: {0}")]
27    /// Log level parse error
28    LevelParseError(String),
29
30    #[error("Unsupported log format: {0}")]
31    /// Unsupported log format
32    UnsupportedFormat(String),
33
34    #[error("Log formatting error: {0}")]
35    /// Log formatting error
36    FormattingError(String),
37
38    #[error("Log rotation error: {0}")]
39    /// Log rotation error
40    RotationError(String),
41
42    #[error("Network error: {0}")]
43    /// Network error
44    NetworkError(String),
45
46    #[error("DateTime parse error: {0}")]
47    /// `DateTime` parse error
48    DateTimeParseError(String),
49
50    #[error("{0}")]
51    /// Custom error
52    Custom(String),
53
54    #[error("Native OS sink failure: {0}")]
55    /// Native OS sink failure
56    NativeSinkError(String),
57}
58
59impl From<crate::commons::error::CommonError> for RlgError {
60    fn from(err: crate::commons::error::CommonError) -> Self {
61        Self::Custom(err.to_string())
62    }
63}
64
65impl RlgError {
66    /// Create a custom error with the given message.
67    #[must_use]
68    pub fn custom<T: fmt::Display>(msg: T) -> Self {
69        Self::Custom(msg.to_string())
70    }
71
72    /// A stable, machine-readable code for this error, such as
73    /// `rlg::io_error`. Suitable for matching in tooling and for
74    /// indexing error documentation.
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use rlg::error::RlgError;
80    /// let err = RlgError::RotationError("disk full".into());
81    /// assert_eq!(err.code(), "rlg::rotation_error");
82    /// ```
83    #[must_use]
84    pub const fn code(&self) -> &'static str {
85        self.diagnostic().0
86    }
87
88    /// A one-line hint on how to resolve this error, when there is
89    /// one.
90    ///
91    /// # Examples
92    ///
93    /// ```
94    /// use rlg::error::RlgError;
95    /// let err = RlgError::LevelParseError("LOUD".into());
96    /// assert!(err.help().unwrap().contains("TRACE"));
97    /// assert!(RlgError::custom("x").help().is_none());
98    /// ```
99    #[must_use]
100    pub const fn help(&self) -> Option<&'static str> {
101        self.diagnostic().1
102    }
103
104    /// Render the error as a multi-line report: the code, the
105    /// message and, when there is one, the help line.
106    ///
107    /// # Examples
108    ///
109    /// ```
110    /// use rlg::error::RlgError;
111    /// let err = RlgError::NetworkError("timeout".into());
112    /// assert_eq!(
113    ///     err.report().to_string(),
114    ///     "error[rlg::network_error]: Network error: timeout\n  \
115    ///      help: Check your network connection or the OTLP collector endpoint.",
116    /// );
117    /// ```
118    #[must_use]
119    pub const fn report(&self) -> Report<'_> {
120        Report(self)
121    }
122
123    /// The code and help line of each variant. One exhaustive match,
124    /// so a new variant cannot compile without a diagnostic.
125    const fn diagnostic(&self) -> Diagnostic {
126        match self {
127            Self::IoError(_) => ("rlg::io_error", Some(HELP_IO)),
128            Self::ConfigError(_) => {
129                ("rlg::config_error", Some(HELP_CONFIG))
130            }
131            Self::FormatParseError(_) => {
132                ("rlg::format_parse_error", Some(HELP_FORMAT_PARSE))
133            }
134            Self::LevelParseError(_) => {
135                ("rlg::level_parse_error", Some(HELP_LEVEL_PARSE))
136            }
137            Self::UnsupportedFormat(_) => (
138                "rlg::unsupported_format",
139                Some(HELP_UNSUPPORTED_FORMAT),
140            ),
141            Self::FormattingError(_) => {
142                ("rlg::formatting_error", Some(HELP_FORMATTING))
143            }
144            Self::RotationError(_) => {
145                ("rlg::rotation_error", Some(HELP_ROTATION))
146            }
147            Self::NetworkError(_) => {
148                ("rlg::network_error", Some(HELP_NETWORK))
149            }
150            Self::DateTimeParseError(_) => {
151                ("rlg::datetime_parse_error", Some(HELP_DATETIME_PARSE))
152            }
153            Self::Custom(_) => ("rlg::custom_error", None),
154            Self::NativeSinkError(_) => {
155                ("rlg::native_sink_failure", Some(HELP_NATIVE_SINK))
156            }
157        }
158    }
159}
160
161/// An error's stable code and optional help line.
162type Diagnostic = (&'static str, Option<&'static str>);
163
164const HELP_IO: &str =
165    "Ensure the log directory exists and is writable.";
166const HELP_CONFIG: &str =
167    "Check your configuration file or environment variables.";
168const HELP_FORMAT_PARSE: &str = "Ensure the format string matches supported variants (JSON, OTLP, MCP, etc.).";
169const HELP_LEVEL_PARSE: &str =
170    "Supported levels: ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL.";
171const HELP_UNSUPPORTED_FORMAT: &str =
172    "Visit docs.rs/rlg for a list of supported industry formats.";
173const HELP_FORMATTING: &str =
174    "This may happen if attributes contain non-serializable data.";
175const HELP_ROTATION: &str =
176    "Ensure RLG has permission to rename or delete old log files.";
177const HELP_NETWORK: &str =
178    "Check your network connection or the OTLP collector endpoint.";
179const HELP_DATETIME_PARSE: &str =
180    "RLG expects RFC 3339 / ISO 8601 timestamps.";
181const HELP_NATIVE_SINK: &str = "Check if systemd-journald is running (Linux) or if 'com.rlg.logger' subsystem is registered (macOS). Ensure RLG_FALLBACK_STDOUT is set if you want to bypass native hooks.";
182
183/// A displayable diagnostic report for an [`RlgError`], returned by
184/// [`RlgError::report`].
185#[derive(Debug, Clone, Copy)]
186pub struct Report<'a>(&'a RlgError);
187
188impl fmt::Display for Report<'_> {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "error[{}]: {}", self.0.code(), self.0)?;
191        if let Some(help) = self.0.help() {
192            write!(f, "\n  help: {help}")?;
193        }
194        Ok(())
195    }
196}
197
198/// Convenience alias: `Result<T, RlgError>`.
199pub type RlgResult<T> = Result<T, RlgError>;
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    #[test]
206    fn test_error_display() {
207        let err =
208            RlgError::FormatParseError("Invalid format".to_string());
209        assert_eq!(
210            err.to_string(),
211            "Log format parse error: Invalid format"
212        );
213    }
214
215    #[test]
216    fn test_custom_error() {
217        let err = RlgError::custom("Custom error message");
218        assert_eq!(err.to_string(), "Custom error message");
219    }
220
221    #[test]
222    fn test_common_error_conversion() {
223        let common_err =
224            crate::commons::error::CommonError::custom("test");
225        let rlg_err: RlgError = common_err.into();
226        assert!(matches!(rlg_err, RlgError::Custom(_)));
227        assert!(rlg_err.to_string().contains("test"));
228    }
229
230    #[test]
231    fn test_config_error_conversion() {
232        let config_err =
233            ConfigError::ValidationError("Test error".to_string());
234        let rlg_err: RlgError = config_err.into();
235        assert!(matches!(rlg_err, RlgError::ConfigError(_)));
236    }
237
238    #[test]
239    fn test_io_error_variant() {
240        let io_err =
241            io::Error::new(io::ErrorKind::NotFound, "file missing");
242        let rlg_err: RlgError = io_err.into();
243        assert!(matches!(rlg_err, RlgError::IoError(_)));
244        assert!(rlg_err.to_string().contains("file missing"));
245    }
246
247    #[test]
248    fn test_format_parse_error_variant() {
249        let err = RlgError::FormatParseError("bad format".into());
250        assert_eq!(
251            err.to_string(),
252            "Log format parse error: bad format"
253        );
254    }
255
256    #[test]
257    fn test_level_parse_error_variant() {
258        let err = RlgError::LevelParseError("bad level".into());
259        assert_eq!(err.to_string(), "Log level parse error: bad level");
260    }
261
262    #[test]
263    fn test_unsupported_format_variant() {
264        let err = RlgError::UnsupportedFormat("XML".into());
265        assert_eq!(err.to_string(), "Unsupported log format: XML");
266    }
267
268    #[test]
269    fn test_formatting_error_variant() {
270        let err = RlgError::FormattingError("template".into());
271        assert_eq!(err.to_string(), "Log formatting error: template");
272    }
273
274    #[test]
275    fn test_rotation_error_variant() {
276        let err = RlgError::RotationError("disk full".into());
277        assert_eq!(err.to_string(), "Log rotation error: disk full");
278    }
279
280    #[test]
281    fn test_network_error_variant() {
282        let err = RlgError::NetworkError("timeout".into());
283        assert_eq!(err.to_string(), "Network error: timeout");
284    }
285
286    #[test]
287    fn test_datetime_parse_error_variant() {
288        let err = RlgError::DateTimeParseError("bad date".into());
289        assert_eq!(err.to_string(), "DateTime parse error: bad date");
290    }
291
292    #[test]
293    fn test_native_sink_error_variant() {
294        let err = RlgError::NativeSinkError("journald down".into());
295        assert_eq!(
296            err.to_string(),
297            "Native OS sink failure: journald down"
298        );
299    }
300
301    #[test]
302    fn test_error_debug_all_variants() {
303        let variants: Vec<RlgError> = vec![
304            RlgError::IoError(io::Error::other("test")),
305            RlgError::ConfigError(ConfigError::ValidationError(
306                "v".into(),
307            )),
308            RlgError::FormatParseError("f".into()),
309            RlgError::LevelParseError("l".into()),
310            RlgError::UnsupportedFormat("u".into()),
311            RlgError::FormattingError("fm".into()),
312            RlgError::RotationError("r".into()),
313            RlgError::NetworkError("n".into()),
314            RlgError::DateTimeParseError("d".into()),
315            RlgError::Custom("c".into()),
316            RlgError::NativeSinkError("ns".into()),
317        ];
318        for err in &variants {
319            let dbg = format!("{err:?}");
320            assert_ne!(dbg, "");
321        }
322    }
323
324    #[test]
325    fn test_every_variant_has_a_distinct_code() {
326        let variants: Vec<RlgError> = vec![
327            RlgError::IoError(io::Error::other("test")),
328            RlgError::ConfigError(ConfigError::ValidationError(
329                "v".into(),
330            )),
331            RlgError::FormatParseError("f".into()),
332            RlgError::LevelParseError("l".into()),
333            RlgError::UnsupportedFormat("u".into()),
334            RlgError::FormattingError("fm".into()),
335            RlgError::RotationError("r".into()),
336            RlgError::NetworkError("n".into()),
337            RlgError::DateTimeParseError("d".into()),
338            RlgError::Custom("c".into()),
339            RlgError::NativeSinkError("ns".into()),
340        ];
341        let codes: std::collections::HashSet<_> =
342            variants.iter().map(RlgError::code).collect();
343        assert_eq!(codes.len(), variants.len());
344        for err in &variants {
345            assert!(err.code().starts_with("rlg::"));
346            let report = err.report().to_string();
347            assert!(
348                report.starts_with(&format!("error[{}]: ", err.code()))
349            );
350            assert_eq!(
351                report.contains("\n  help: "),
352                err.help().is_some()
353            );
354        }
355    }
356
357    #[test]
358    fn test_custom_error_has_no_help() {
359        let err = RlgError::custom("boom");
360        assert_eq!(err.help(), None);
361        assert_eq!(
362            err.report().to_string(),
363            "error[rlg::custom_error]: boom"
364        );
365    }
366
367    #[test]
368    fn test_error_is_std_error() {
369        let err = RlgError::NetworkError("test".into());
370        let _: &dyn std::error::Error = &err;
371    }
372
373    #[test]
374    fn test_rlg_result_ok() {
375        let r: RlgResult<i32> = Ok(42);
376        assert!(matches!(r, Ok(42)));
377    }
378
379    #[test]
380    fn test_rlg_result_err() {
381        let r: RlgResult<i32> = Err(RlgError::custom("fail"));
382        assert!(r.is_err());
383    }
384}