1use crate::config::ConfigError;
7use std::fmt;
8use std::io;
9use thiserror::Error;
10
11#[derive(Error, Debug)]
12pub enum RlgError {
14 #[error("I/O error: {0}")]
15 IoError(#[from] io::Error),
17
18 #[error("Configuration error: {0}")]
19 ConfigError(#[from] ConfigError),
21
22 #[error("Log format parse error: {0}")]
23 FormatParseError(String),
25
26 #[error("Log level parse error: {0}")]
27 LevelParseError(String),
29
30 #[error("Unsupported log format: {0}")]
31 UnsupportedFormat(String),
33
34 #[error("Log formatting error: {0}")]
35 FormattingError(String),
37
38 #[error("Log rotation error: {0}")]
39 RotationError(String),
41
42 #[error("Network error: {0}")]
43 NetworkError(String),
45
46 #[error("DateTime parse error: {0}")]
47 DateTimeParseError(String),
49
50 #[error("{0}")]
51 Custom(String),
53
54 #[error("Native OS sink failure: {0}")]
55 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 #[must_use]
68 pub fn custom<T: fmt::Display>(msg: T) -> Self {
69 Self::Custom(msg.to_string())
70 }
71
72 #[must_use]
84 pub const fn code(&self) -> &'static str {
85 self.diagnostic().0
86 }
87
88 #[must_use]
100 pub const fn help(&self) -> Option<&'static str> {
101 self.diagnostic().1
102 }
103
104 #[must_use]
119 pub const fn report(&self) -> Report<'_> {
120 Report(self)
121 }
122
123 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
161type 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#[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
198pub 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}