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;
7#[cfg(feature = "miette")]
8use miette::Diagnostic;
9use std::fmt;
10use std::io;
11use thiserror::Error;
12
13#[derive(Error, Debug)]
14#[cfg_attr(feature = "miette", derive(Diagnostic))]
15/// Error variants for the RLG logging pipeline.
16pub enum RlgError {
17    #[error("I/O error: {0}")]
18    #[cfg_attr(
19        feature = "miette",
20        diagnostic(
21            code(rlg::io_error),
22            help("Ensure the log directory exists and is writable.")
23        )
24    )]
25    /// I/O error
26    IoError(#[from] io::Error),
27
28    #[error("Configuration error: {0}")]
29    #[cfg_attr(
30        feature = "miette",
31        diagnostic(
32            code(rlg::config_error),
33            help(
34                "Check your configuration file or environment variables."
35            )
36        )
37    )]
38    /// Configuration error
39    ConfigError(#[from] ConfigError),
40
41    #[error("Log format parse error: {0}")]
42    #[cfg_attr(
43        feature = "miette",
44        diagnostic(
45            code(rlg::format_parse_error),
46            help(
47                "Ensure the format string matches supported variants (JSON, OTLP, MCP, etc.)."
48            )
49        )
50    )]
51    /// Log format parse error
52    FormatParseError(String),
53
54    #[error("Log level parse error: {0}")]
55    #[cfg_attr(
56        feature = "miette",
57        diagnostic(
58            code(rlg::level_parse_error),
59            help(
60                "Supported levels: ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL."
61            )
62        )
63    )]
64    /// Log level parse error
65    LevelParseError(String),
66
67    #[error("Unsupported log format: {0}")]
68    #[cfg_attr(
69        feature = "miette",
70        diagnostic(
71            code(rlg::unsupported_format),
72            help(
73                "Visit docs.rs/rlg for a list of supported industry formats."
74            )
75        )
76    )]
77    /// Unsupported log format
78    UnsupportedFormat(String),
79
80    #[error("Log formatting error: {0}")]
81    #[cfg_attr(
82        feature = "miette",
83        diagnostic(
84            code(rlg::formatting_error),
85            help(
86                "This may happen if attributes contain non-serializable data."
87            )
88        )
89    )]
90    /// Log formatting error
91    FormattingError(String),
92
93    #[error("Log rotation error: {0}")]
94    #[cfg_attr(
95        feature = "miette",
96        diagnostic(
97            code(rlg::rotation_error),
98            help(
99                "Ensure RLG has permission to rename or delete old log files."
100            )
101        )
102    )]
103    /// Log rotation error
104    RotationError(String),
105
106    #[error("Network error: {0}")]
107    #[cfg_attr(
108        feature = "miette",
109        diagnostic(
110            code(rlg::network_error),
111            help(
112                "Check your network connection or the OTLP collector endpoint."
113            )
114        )
115    )]
116    /// Network error
117    NetworkError(String),
118
119    #[error("DateTime parse error: {0}")]
120    #[cfg_attr(
121        feature = "miette",
122        diagnostic(
123            code(rlg::datetime_parse_error),
124            help("RLG expects RFC 3339 / ISO 8601 timestamps.")
125        )
126    )]
127    /// `DateTime` parse error
128    DateTimeParseError(String),
129
130    #[error("{0}")]
131    #[cfg_attr(feature = "miette", diagnostic(code(rlg::custom_error)))]
132    /// Custom error
133    Custom(String),
134
135    #[error("Native OS sink failure: {0}")]
136    #[cfg_attr(
137        feature = "miette",
138        diagnostic(
139            code(rlg::native_sink_failure),
140            help(
141                "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."
142            )
143        )
144    )]
145    /// Native OS sink failure
146    NativeSinkError(String),
147}
148
149impl From<crate::commons::error::CommonError> for RlgError {
150    fn from(err: crate::commons::error::CommonError) -> Self {
151        Self::Custom(err.to_string())
152    }
153}
154
155impl RlgError {
156    /// Create a custom error with the given message.
157    #[must_use]
158    pub fn custom<T: fmt::Display>(msg: T) -> Self {
159        Self::Custom(msg.to_string())
160    }
161}
162
163/// Convenience alias: `Result<T, RlgError>`.
164pub type RlgResult<T> = Result<T, RlgError>;
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_error_display() {
172        let err =
173            RlgError::FormatParseError("Invalid format".to_string());
174        assert_eq!(
175            err.to_string(),
176            "Log format parse error: Invalid format"
177        );
178    }
179
180    #[test]
181    fn test_custom_error() {
182        let err = RlgError::custom("Custom error message");
183        assert_eq!(err.to_string(), "Custom error message");
184    }
185
186    #[test]
187    fn test_common_error_conversion() {
188        let common_err =
189            crate::commons::error::CommonError::custom("test");
190        let rlg_err: RlgError = common_err.into();
191        assert!(matches!(rlg_err, RlgError::Custom(_)));
192        assert!(rlg_err.to_string().contains("test"));
193    }
194
195    #[test]
196    fn test_config_error_conversion() {
197        let config_err =
198            ConfigError::ValidationError("Test error".to_string());
199        let rlg_err: RlgError = config_err.into();
200        assert!(matches!(rlg_err, RlgError::ConfigError(_)));
201    }
202
203    #[test]
204    fn test_io_error_variant() {
205        let io_err =
206            io::Error::new(io::ErrorKind::NotFound, "file missing");
207        let rlg_err: RlgError = io_err.into();
208        assert!(matches!(rlg_err, RlgError::IoError(_)));
209        assert!(rlg_err.to_string().contains("file missing"));
210    }
211
212    #[test]
213    fn test_format_parse_error_variant() {
214        let err = RlgError::FormatParseError("bad format".into());
215        assert_eq!(
216            err.to_string(),
217            "Log format parse error: bad format"
218        );
219    }
220
221    #[test]
222    fn test_level_parse_error_variant() {
223        let err = RlgError::LevelParseError("bad level".into());
224        assert_eq!(err.to_string(), "Log level parse error: bad level");
225    }
226
227    #[test]
228    fn test_unsupported_format_variant() {
229        let err = RlgError::UnsupportedFormat("XML".into());
230        assert_eq!(err.to_string(), "Unsupported log format: XML");
231    }
232
233    #[test]
234    fn test_formatting_error_variant() {
235        let err = RlgError::FormattingError("template".into());
236        assert_eq!(err.to_string(), "Log formatting error: template");
237    }
238
239    #[test]
240    fn test_rotation_error_variant() {
241        let err = RlgError::RotationError("disk full".into());
242        assert_eq!(err.to_string(), "Log rotation error: disk full");
243    }
244
245    #[test]
246    fn test_network_error_variant() {
247        let err = RlgError::NetworkError("timeout".into());
248        assert_eq!(err.to_string(), "Network error: timeout");
249    }
250
251    #[test]
252    fn test_datetime_parse_error_variant() {
253        let err = RlgError::DateTimeParseError("bad date".into());
254        assert_eq!(err.to_string(), "DateTime parse error: bad date");
255    }
256
257    #[test]
258    fn test_native_sink_error_variant() {
259        let err = RlgError::NativeSinkError("journald down".into());
260        assert_eq!(
261            err.to_string(),
262            "Native OS sink failure: journald down"
263        );
264    }
265
266    #[test]
267    fn test_error_debug_all_variants() {
268        let variants: Vec<RlgError> = vec![
269            RlgError::IoError(io::Error::other("test")),
270            RlgError::ConfigError(ConfigError::ValidationError(
271                "v".into(),
272            )),
273            RlgError::FormatParseError("f".into()),
274            RlgError::LevelParseError("l".into()),
275            RlgError::UnsupportedFormat("u".into()),
276            RlgError::FormattingError("fm".into()),
277            RlgError::RotationError("r".into()),
278            RlgError::NetworkError("n".into()),
279            RlgError::DateTimeParseError("d".into()),
280            RlgError::Custom("c".into()),
281            RlgError::NativeSinkError("ns".into()),
282        ];
283        for err in &variants {
284            let dbg = format!("{err:?}");
285            assert!(!dbg.is_empty());
286        }
287    }
288
289    #[test]
290    fn test_error_is_std_error() {
291        let err = RlgError::NetworkError("test".into());
292        let _: &dyn std::error::Error = &err;
293    }
294
295    #[test]
296    fn test_rlg_result_ok() {
297        let r: RlgResult<i32> = Ok(42);
298        assert!(matches!(r, Ok(42)));
299    }
300
301    #[test]
302    fn test_rlg_result_err() {
303        let r: RlgResult<i32> = Err(RlgError::custom("fail"));
304        assert!(r.is_err());
305    }
306}