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}