Skip to main content

tracing_result/
lib.rs

1#![cfg_attr(unstable_never_type, feature(never_type))]
2#![cfg_attr(unstable_try_trait_v2, feature(try_trait_v2))]
3
4//! A library for ergonomic error handling with tracing support.
5//!
6//! This crate provides a wrapper around [`Result`], [`Option`] & any other types
7//! which implement [`Try`] that automatically emits tracing messages at configurable log levels
8//! when unpacked via the `?` operator.
9//!
10//! # Overview
11//!
12//! The [`Trace`] trait extends all `Try`-types `T` with methods that wrap it in a [`Traced<T>`]
13//! with a custom tracing message. The message is emitted automatically when the [`Traced<T>`] is
14//! used with the `?` operator.
15//!
16//! Simply `use tracing_result::Trace` to bring the methods into scope. The blanket implementation
17//! ensures availability on all `Try`-types, not only `Result`& `Option`.
18//!
19//! There are two families of methods:
20//! - **`or_*` methods** ([`or_error`][Trace::or_error], [`or_warn`][Trace::or_warn],
21//!   [`or_info`][Trace::or_info], [`or_debug`][Trace::or_debug], [`or_trace`][Trace::or_trace]):
22//!   Log the message when an **`Err`**/**`None`** etc. is unpacked
23//! - **`and_*` methods** ([`and_error`][Trace::and_error], [`and_warn`][Trace::and_warn],
24//!   [`and_info`][Trace::and_info], [`and_debug`][Trace::and_debug], [`and_trace`][Trace::and_trace]):
25//!   Log the message when an **`Ok`**/**`Some`** etc. is unpacked
26//!
27//! # Examples
28//!
29//! ## `Traced<Result>`
30//!
31//! ```
32//! use std::io;
33//! use tracing_result::Trace;
34//!
35//! fn might_fail() -> io::Result<u32> {
36//!     Err(io::Error::other("something went wrong"))
37//! }
38//!
39//! fn compute() -> io::Result<u32> {
40//!     // If might_fail() returns Err, "Failed to compute value" will be logged as a warning
41//!     might_fail().or_warn("Failed to compute value")?;
42//!     Ok(42)
43//! }
44//!
45//! fn unexpected_success() -> io::Result<u32> {
46//!     // If Ok is returned, "Unexpected: computation succeeded" will be logged as a warning
47//!     let res = io::Result::Ok(42).and_warn("Unexpected: computation succeeded")?;
48//!     Ok(res)
49//! }
50//! ```
51//!
52//! ## `Traced<Option>`
53//!
54//! ```
55//! use tracing_result::Trace;
56//! use try_v2::{Extract, Transform};
57//!
58//! #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
59//! struct Data {
60//!     value: Option<i32>,
61//! }
62//!
63//! let data: Vec<Data> = Vec::new();
64//!
65//! let first_value = data
66//!     .first()
67//!     .or_warn("No entries found") // Emitted if `None`
68//!     .and_then(|d| d.value) // `Transform` methods call `Try::branch` and therefore emit
69//!     .or_warn("No value in first entry") // Emitted if `None`
70//!     .unwrap_or_default(); // `Extract` methods call `Try::branch` and therefore emit
71//!
72//! assert_eq!(first_value, 0);
73//! ```
74
75use std::ops::{ControlFlow, FromResidual, Try};
76use tracing::Level;
77use try_v2::{Extract, Transform};
78
79use crate::TracingConfig::{OnOutput, OnResidual};
80
81/// The [`Trace`] trait extends all `Try`-types `T` with methods that wrap it in a [`Traced<T>`]
82/// with a custom tracing message. The message is emitted automatically when the [`Traced<T>`] is
83/// used with the `?` operator.
84///
85/// Simply `use tracing_result::Trace` to bring the methods into scope. The blanket implementation
86/// ensures availability on all `Try`-types, not only `Result`& `Option`.
87///
88/// There are two families of methods:
89/// - **`or_*` methods** ([`or_error`][Trace::or_error], [`or_warn`][Trace::or_warn],
90///   [`or_info`][Trace::or_info], [`or_debug`][Trace::or_debug], [`or_trace`][Trace::or_trace]):
91///   Log the message when an **`Err`**/**`None`** etc. is unpacked
92/// - **`and_*` methods** ([`and_error`][Trace::and_error], [`and_warn`][Trace::and_warn],
93///   [`and_info`][Trace::and_info], [`and_debug`][Trace::and_debug], [`and_trace`][Trace::and_trace]):
94///   Log the message when an **`Ok`**/**`Some`** etc. is unpacked
95///
96/// # Examples
97///
98/// ## `Traced<Result>`
99///
100/// ```
101/// use std::io;
102/// use tracing_result::Trace;
103///
104/// fn might_fail() -> io::Result<u32> {
105///     Err(io::Error::other("something went wrong"))
106/// }
107///
108/// fn compute() -> io::Result<u32> {
109///     // If might_fail() returns Err, "Failed to compute value" will be logged as a warning
110///     might_fail().or_warn("Failed to compute value")?;
111///     Ok(42)
112/// }
113///
114/// fn unexpected_success() -> io::Result<u32> {
115///     // If Ok is returned, "Unexpected: computation succeeded" will be logged as a warning
116///     let res = io::Result::Ok(42).and_warn("Unexpected: computation succeeded")?;
117///     Ok(res)
118/// }
119/// ```
120///
121/// ## `Traced<Option>`
122///
123/// ```
124/// use tracing_result::Trace;
125/// use try_v2::{Extract, Transform};
126///
127/// #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
128/// struct Data {
129///     value: Option<i32>,
130/// }
131///
132/// let data: Vec<Data> = Vec::new();
133///
134/// let first_value = data
135///     .first()
136///     .or_warn("No entries found") // Emitted if `None`
137///     .and_then(|d| d.value) // `Transform` methods call `Try::branch` and therefore emit
138///     .or_warn("No value in first entry") // Emitted if `None`
139///     .unwrap_or_default(); // `Extract` methods call `Try::branch` and therefore emit
140///
141/// assert_eq!(first_value, 0);
142/// ```
143pub trait Trace<T: Try> {
144    /// Attaches an error-level message to the `Residual` case.
145    fn or_error(self, name: &'static str) -> Traced<T>;
146
147    /// Attaches an error-level message to the `Output` case.
148    fn and_error(self, name: &'static str) -> Traced<T>;
149
150    /// Attaches a warning message to the `Residual` case.
151    fn or_warn(self, name: &'static str) -> Traced<T>;
152
153    /// Attaches a warning message to the `Output` case.
154    fn and_warn(self, name: &'static str) -> Traced<T>;
155
156    /// Attaches an info message to the `Residual` case.
157    fn or_info(self, name: &'static str) -> Traced<T>;
158
159    /// Attaches an info message to the `Output` case.
160    fn and_info(self, name: &'static str) -> Traced<T>;
161
162    /// Attaches a debug message to the `Residual` case.
163    fn or_debug(self, name: &'static str) -> Traced<T>;
164
165    /// Attaches a debug message to the `Output` case.
166    fn and_debug(self, name: &'static str) -> Traced<T>;
167
168    /// Attaches a trace-level message to the `Residual` case.
169    fn or_trace(self, name: &'static str) -> Traced<T>;
170
171    /// Attaches a trace-level message to the `Output` case.
172    fn and_trace(self, name: &'static str) -> Traced<T>;
173}
174
175impl<T: Try> Trace<T> for T {
176    fn or_error(self, name: &'static str) -> Traced<T> {
177        Traced {
178            inner: self,
179            event: TracingConfig::OnResidual(Event {
180                level: Level::ERROR,
181                message: name,
182            }),
183        }
184    }
185
186    fn and_error(self, name: &'static str) -> Traced<T> {
187        Traced {
188            inner: self,
189            event: TracingConfig::OnOutput(Event {
190                level: Level::ERROR,
191                message: name,
192            }),
193        }
194    }
195
196    fn or_warn(self, name: &'static str) -> Traced<T> {
197        Traced {
198            inner: self,
199            event: TracingConfig::OnResidual(Event {
200                level: Level::WARN,
201                message: name,
202            }),
203        }
204    }
205
206    fn and_warn(self, name: &'static str) -> Traced<T> {
207        Traced {
208            inner: self,
209            event: TracingConfig::OnOutput(Event {
210                level: Level::WARN,
211                message: name,
212            }),
213        }
214    }
215
216    fn or_info(self, name: &'static str) -> Traced<T> {
217        Traced {
218            inner: self,
219            event: TracingConfig::OnResidual(Event {
220                level: Level::INFO,
221                message: name,
222            }),
223        }
224    }
225
226    fn and_info(self, name: &'static str) -> Traced<T> {
227        Traced {
228            inner: self,
229            event: TracingConfig::OnOutput(Event {
230                level: Level::INFO,
231                message: name,
232            }),
233        }
234    }
235
236    fn or_debug(self, name: &'static str) -> Traced<T> {
237        Traced {
238            inner: self,
239            event: TracingConfig::OnResidual(Event {
240                level: Level::DEBUG,
241                message: name,
242            }),
243        }
244    }
245
246    fn and_debug(self, name: &'static str) -> Traced<T> {
247        Traced {
248            inner: self,
249            event: TracingConfig::OnOutput(Event {
250                level: Level::DEBUG,
251                message: name,
252            }),
253        }
254    }
255
256    fn or_trace(self, name: &'static str) -> Traced<T> {
257        Traced {
258            inner: self,
259            event: TracingConfig::OnResidual(Event {
260                level: Level::TRACE,
261                message: name,
262            }),
263        }
264    }
265
266    fn and_trace(self, name: &'static str) -> Traced<T> {
267        Traced {
268            inner: self,
269            event: TracingConfig::OnOutput(Event {
270                level: Level::TRACE,
271                message: name,
272            }),
273        }
274    }
275}
276
277/// The wrapper around a `Try`-type with associated tracing event. You will usually not
278/// interact directly with this type but rather with the trait [`Trace`].
279#[must_use = "The wrapped type may be an `Err`, `None`, or other `Residual` which requires handling."]
280pub struct Traced<T>
281where
282    T: Try,
283{
284    inner: T,
285    event: TracingConfig,
286}
287
288/// Calling any of the extraction functions *will emit* the enclosed tracing entries related
289/// to the relevant variant.
290impl<T, O> Extract<O> for Traced<T> where T: Try<Output = O> {}
291
292/// Calling any of the transformation functions *will emit* the enclosed tracing entries related
293/// to the relevant variant.
294impl<T, O> Transform<O> for Traced<T> where T: Try<Output = O> {}
295
296/// # Note
297///
298/// - Any attached events are emitted when `branch` is called and all wrappings are
299///   removed, leaving either the `Output` or `Resdiual` of the wrapped `Try`-type.
300///   For example: using `?` on a `Traced<Traced<Option<T>>>` will result in either a
301///   `T` or `None`.
302/// - `Traced<T>` does not implement its own `Output` or `Residual` types but immediately
303///   unwraps to the `Output` / `Residual` of the wrapped `Try`-type.
304impl<T: Try> Try for Traced<T> {
305    type Output = T::Output;
306
307    type Residual = T::Residual;
308
309    fn from_output(output: Self::Output) -> Self {
310        Self {
311            inner: Try::from_output(output),
312            event: Default::default(),
313        }
314    }
315
316    /// Used in `?` to decide whether the operator should produce a value
317    /// (because this returned [`ControlFlow::Continue`])
318    /// or propagate a value back to the caller
319    /// (because this returned [`ControlFlow::Break`]).
320    ///
321    /// # Note
322    ///
323    /// Any attached events are emitted when `branch` is called and all wrappings are
324    /// removed, leaving either the `Output` or `Resdiual` of the wrapped `Try`-type.
325    /// For example: using `?` on a `Traced<Traced<Option<T>>>` will result in either a
326    /// `T` or `None`.
327    fn branch(self) -> ControlFlow<Self::Residual, Self::Output> {
328        match self.inner.branch() {
329            ControlFlow::Continue(output) => {
330                self.event.emit_output();
331                ControlFlow::Continue(output)
332            }
333            ControlFlow::Break(residual) => {
334                self.event.emit_residual();
335                ControlFlow::Break(residual)
336            }
337        }
338    }
339}
340
341/// # Note
342///
343/// - The `Traced<T>` returned from ` try { Traced<T>? } ` will have no attached tracing
344///   events, as all will be emitted by the `?` inside the `try` block
345/// - `FromResidual` is primarily implemented as a formality, normal usage will likely be
346///   in the form `try bikeshed T { Traced<T>? }`
347impl<T: Try> FromResidual for Traced<T> {
348    fn from_residual(residual: <Self as Try>::Residual) -> Self {
349        Self {
350            inner: FromResidual::from_residual(residual),
351            event: Default::default(),
352        }
353    }
354}
355
356pub type TracedResult<T, E> = Traced<Result<T, E>>;
357pub type TracedOption<T> = Traced<Option<T>>;
358
359impl<T, E> TracedResult<T, E> {
360    /// Converts from `TracedResult<T, E>` to `Option<E>`, emitting any tracing entry in the process.
361    pub fn err(self) -> Option<E> {
362        match self.branch() {
363            ControlFlow::Continue(_) => None,
364            ControlFlow::Break(Result::Err(err)) => Some(err),
365        }
366    }
367
368    /// Converts from `TracedResult<T, E>` to `Option<T>`, emitting any tracing entry in the process.
369    pub fn ok(self) -> Option<T> {
370        self.output()
371    }
372}
373
374/// Configuration for tracing log level and message.
375#[derive(Debug, Clone, Copy)]
376struct Event {
377    /// The tracing level to use when logging.
378    level: Level,
379    /// The message to log.
380    message: &'static str,
381}
382
383impl Event {
384    fn emit(&self) {
385        match self.level {
386            Level::ERROR => tracing::error!("{}", self.message),
387            Level::WARN => tracing::warn!("{}", self.message),
388            Level::INFO => tracing::info!("{}", self.message),
389            Level::DEBUG => tracing::debug!("{}", self.message),
390            Level::TRACE => tracing::trace!("{}", self.message),
391        }
392    }
393}
394
395#[derive(Debug, Clone, Copy, Default)]
396enum TracingConfig {
397    OnOutput(Event),
398    OnResidual(Event),
399    #[default]
400    None,
401}
402
403impl TracingConfig {
404    fn emit_output(&self) {
405        if let OnOutput(event) = self {
406            event.emit();
407        }
408    }
409
410    fn emit_residual(&self) {
411        if let OnResidual(event) = self {
412            event.emit();
413        }
414    }
415}
416
417#[cfg(test)]
418mod tests {
419    use std::io;
420
421    use tracing_test::traced_test;
422
423    use super::*;
424
425    /// Tests that `or_error` on an Ok result does not log.
426    #[traced_test]
427    #[test]
428    fn or_error_ok() {
429        fn no_error() -> io::Result<()> {
430            io::Result::Ok(()).or_error("should not log")?;
431            Ok(())
432        }
433
434        assert!(no_error().is_ok());
435        assert!(!logs_contain("should not log"));
436    }
437
438    /// Tests that `or_error` on an Err result logs at ERROR level.
439    #[traced_test]
440    #[test]
441    fn or_error_err() {
442        fn err() -> io::Result<()> {
443            Err(io::Error::other("oops")).or_error("error message")?;
444            Ok(())
445        }
446
447        assert!(err().is_err());
448        assert!(logs_contain("error message"));
449    }
450
451    /// Tests that `and_error` on an Ok result logs at ERROR level.
452    #[traced_test]
453    #[test]
454    fn and_error_ok() {
455        fn ok() -> io::Result<()> {
456            io::Result::Ok(()).and_error("error on ok")?;
457            Ok(())
458        }
459
460        assert!(ok().is_ok());
461        assert!(logs_contain("error on ok"));
462    }
463
464    /// Tests that `and_error` on an Err result does not log.
465    #[traced_test]
466    #[test]
467    fn and_error_err() {
468        fn err() -> io::Result<()> {
469            Err(io::Error::other("oops")).and_error("should not log")?;
470            Ok(())
471        }
472
473        assert!(err().is_err());
474        assert!(!logs_contain("should not log"));
475    }
476
477    /// Tests that `or_warn` on an Ok result does not log.
478    #[traced_test]
479    #[test]
480    fn or_warn_ok() {
481        fn no_error() -> io::Result<()> {
482            io::Result::Ok(()).or_warn("stuff")?;
483            Ok(())
484        }
485
486        assert!(no_error().is_ok());
487        assert!(!logs_contain("stuff"));
488    }
489
490    /// Tests that `or_warn` on an Err result logs the warning message.
491    #[traced_test]
492    #[test]
493    fn or_warn_err() {
494        fn err() -> io::Result<()> {
495            Err(io::Error::other("oops")).or_warn("stuff")?;
496            Ok(())
497        }
498
499        assert!(err().is_err());
500        assert!(!logs_contain("name"));
501        assert!(logs_contain("stuff"));
502    }
503
504    /// Tests that `and_warn` on an Ok result logs the warning message.
505    #[traced_test]
506    #[test]
507    fn and_warn_ok() {
508        fn ok() -> io::Result<()> {
509            io::Result::Ok(()).and_warn("ok warn")?;
510            Ok(())
511        }
512
513        assert!(ok().is_ok());
514        assert!(logs_contain("ok warn"));
515    }
516
517    /// Tests that `and_warn` on an Err result does not log.
518    #[traced_test]
519    #[test]
520    fn and_warn_err() {
521        fn err() -> io::Result<()> {
522            Err(io::Error::other("oops")).and_warn("should not log")?;
523            Ok(())
524        }
525
526        assert!(err().is_err());
527        assert!(!logs_contain("should not log"));
528    }
529
530    /// Tests that `or_debug` on an Ok result does not log.
531    #[traced_test]
532    #[test]
533    fn or_debug_ok() {
534        fn no_error() -> io::Result<()> {
535            io::Result::Ok(()).or_debug("should not log")?;
536            Ok(())
537        }
538
539        assert!(no_error().is_ok());
540        assert!(!logs_contain("should not log"));
541    }
542
543    /// Tests that `or_debug` on an Err result logs at DEBUG level.
544    #[traced_test]
545    #[test]
546    fn or_debug_err() {
547        fn err() -> io::Result<()> {
548            Err(io::Error::other("oops")).or_debug("debug message")?;
549            Ok(())
550        }
551
552        assert!(err().is_err());
553        assert!(logs_contain("debug message"));
554    }
555
556    /// Tests that `and_debug` on an Ok result logs at DEBUG level.
557    #[traced_test]
558    #[test]
559    fn and_debug_ok() {
560        fn ok() -> io::Result<()> {
561            io::Result::Ok(()).and_debug("debug on ok")?;
562            Ok(())
563        }
564
565        assert!(ok().is_ok());
566        assert!(logs_contain("debug on ok"));
567    }
568
569    /// Tests that `and_debug` on an Err result does not log.
570    #[traced_test]
571    #[test]
572    fn and_debug_err() {
573        fn err() -> io::Result<()> {
574            Err(io::Error::other("oops")).and_debug("should not log")?;
575            Ok(())
576        }
577
578        assert!(err().is_err());
579        assert!(!logs_contain("should not log"));
580    }
581
582    /// Tests that `or_trace` on an Ok result does not log.
583    #[traced_test]
584    #[test]
585    fn or_trace_ok() {
586        fn no_error() -> io::Result<()> {
587            io::Result::Ok(()).or_trace("should not log")?;
588            Ok(())
589        }
590
591        assert!(no_error().is_ok());
592        assert!(!logs_contain("should not log"));
593    }
594
595    /// Tests that `or_trace` on an Err result logs at TRACE level.
596    #[traced_test]
597    #[test]
598    fn or_trace_err() {
599        fn err() -> io::Result<()> {
600            Err(io::Error::other("oops")).or_trace("trace message")?;
601            Ok(())
602        }
603
604        assert!(err().is_err());
605        assert!(logs_contain("trace message"));
606    }
607
608    /// Tests that `and_trace` on an Ok result logs at TRACE level.
609    #[traced_test]
610    #[test]
611    fn and_trace_ok() {
612        fn ok() -> io::Result<()> {
613            io::Result::Ok(()).and_trace("trace on ok")?;
614            Ok(())
615        }
616
617        assert!(ok().is_ok());
618        assert!(logs_contain("trace on ok"));
619    }
620
621    /// Tests that `and_trace` on an Err result does not log.
622    #[traced_test]
623    #[test]
624    fn and_trace_err() {
625        fn err() -> io::Result<()> {
626            Err(io::Error::other("oops")).and_trace("should not log")?;
627            Ok(())
628        }
629
630        assert!(err().is_err());
631        assert!(!logs_contain("should not log"));
632    }
633
634    /// Tests that `ok` on an Ok(v) with no log entry returns Some(v) and does not emit.
635    #[traced_test]
636    #[test]
637    fn ok_or_ok() {
638        let good = io::Result::Ok(5).or_warn("should not log");
639        let v = good.ok();
640        assert_eq!(v, Some(5));
641        assert!(!logs_contain("should not log"));
642    }
643
644    #[traced_test]
645    #[test]
646    fn ok_and_ok() {
647        let good = io::Result::Ok(5).and_warn("should log");
648        let v = good.ok();
649        assert_eq!(v, Some(5));
650        assert!(logs_contain("should log"));
651    }
652
653    #[traced_test]
654    #[test]
655    fn err_or_err() {
656        let err: TracedResult<i32, _> = Err(io::Error::other("oops")).or_warn("should log");
657        let v = err.err();
658        assert_eq!(v.map(|e| e.to_string()), Some("oops".to_string()));
659        assert!(logs_contain("should log"));
660    }
661
662    #[traced_test]
663    #[test]
664    fn err_and_err() {
665        let err: TracedResult<i32, _> = Err(io::Error::other("oops")).and_warn("should not log");
666        let v = err.err();
667        assert_eq!(v.map(|e| e.to_string()), Some("oops".to_string()));
668        assert!(!logs_contain("should not log"));
669    }
670
671    #[traced_test]
672    #[test]
673    fn err_conversion() {
674        fn convert() -> Result<(), u32> {
675            let err: Result<(), u16> = Err(5);
676            err.or_error("conversion")?;
677            Ok(())
678        }
679        assert!(convert().is_err());
680        assert!(logs_contain("conversion"));
681    }
682
683    #[traced_test]
684    #[test]
685    fn option() {
686        fn opt() -> Option<usize> {
687            let n = Some(5).or_warn("wibble")?;
688            Some(n)
689        }
690
691        assert_eq!(opt(), Some(5));
692        assert!(!logs_contain("wibble"));
693    }
694
695    #[traced_test]
696    #[test]
697    fn none_and_then() {
698        let _: Option<usize> = Option::<usize>::None
699            .or_warn("wibble")
700            .and_then(|x| Some(x + 1));
701        assert!(logs_contain("wibble"));
702    }
703
704    #[traced_test]
705    #[test]
706    fn some_and_then() {
707        let n = Some(4).or_warn("wibble").and_then(|x| Some(x + 1));
708        assert_eq!(n, Some(5));
709        assert!(!logs_contain("wibble"));
710    }
711
712    #[traced_test]
713    #[test]
714    fn chain() {
715        fn chained() -> Option<usize> {
716            let n = Some(4).or_warn("empty").and_debug("full")?;
717            Some(n + 1)
718        }
719        assert_eq!(chained(), Some(5));
720        assert!(logs_contain("full"));
721        assert!(!logs_contain("empty"));
722    }
723}