1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
//! # Logging.

#![allow(missing_docs)]

use crate::context::Context;

#[macro_export]
macro_rules! info {
    ($ctx:expr,  $msg:expr) => {
        info!($ctx, $msg,)
    };
    ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
        let formatted = format!($msg, $($args),*);
        let full = format!("{file}:{line}: {msg}",
                           file = file!(),
                           line = line!(),
                           msg = &formatted);
        $ctx.emit_event($crate::EventType::Info(full));
    }};
}

#[macro_export]
macro_rules! warn {
    ($ctx:expr, $msg:expr) => {
        warn!($ctx, $msg,)
    };
    ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
        let formatted = format!($msg, $($args),*);
        let full = format!("{file}:{line}: {msg}",
                           file = file!(),
                           line = line!(),
                           msg = &formatted);
        $ctx.emit_event($crate::EventType::Warning(full));
    }};
}

#[macro_export]
macro_rules! error {
    ($ctx:expr, $msg:expr) => {
        error!($ctx, $msg,)
    };
    ($ctx:expr, $msg:expr, $($args:expr),* $(,)?) => {{
        let formatted = format!($msg, $($args),*);
        $ctx.set_last_error(&formatted);
        $ctx.emit_event($crate::EventType::Error(formatted));
    }};
}

impl Context {
    /// Set last error string.
    /// Implemented as blocking as used from macros in different, not always async blocks.
    pub fn set_last_error(&self, error: &str) {
        let mut last_error = self.last_error.write().unwrap();
        *last_error = error.to_string();
    }

    /// Get last error string.
    pub fn get_last_error(&self) -> String {
        let last_error = &*self.last_error.read().unwrap();
        last_error.clone()
    }
}

pub trait LogExt<T, E>
where
    Self: std::marker::Sized,
{
    /// Emits a warning if the receiver contains an Err value.
    ///
    /// Thanks to the [track_caller](https://blog.rust-lang.org/2020/08/27/Rust-1.46.0.html#track_caller)
    /// feature, the location of the caller is printed to the log, just like with the warn!() macro.
    ///
    /// Unfortunately, the track_caller feature does not work on async functions (as of Rust 1.50).
    /// Once it is, you can add `#[track_caller]` to helper functions that use one of the log helpers here
    /// so that the location of the caller can be seen in the log. (this won't work with the macros,
    /// like warn!(), since the file!() and line!() macros don't work with track_caller)
    /// See <https://github.com/rust-lang/rust/issues/78840> for progress on this.
    #[track_caller]
    fn log_err(self, context: &Context) -> Result<T, E>;
}

impl<T, E: std::fmt::Display> LogExt<T, E> for Result<T, E> {
    #[track_caller]
    fn log_err(self, context: &Context) -> Result<T, E> {
        if let Err(e) = &self {
            let location = std::panic::Location::caller();

            // We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
            let full = format!(
                "{file}:{line}: {e:#}",
                file = location.file(),
                line = location.line(),
                e = e
            );
            // We can't use the warn!() macro here as the file!() and line!() macros
            // don't work with #[track_caller]
            context.emit_event(crate::EventType::Warning(full));
        };
        self
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result;

    use crate::test_utils::TestContext;

    #[tokio::test(flavor = "multi_thread", worker_threads = 2)]
    async fn test_get_last_error() -> Result<()> {
        let t = TestContext::new().await;

        assert_eq!(t.get_last_error(), "");

        error!(t, "foo-error");
        assert_eq!(t.get_last_error(), "foo-error");

        warn!(t, "foo-warning");
        assert_eq!(t.get_last_error(), "foo-error");

        info!(t, "foo-info");
        assert_eq!(t.get_last_error(), "foo-error");

        error!(t, "bar-error");
        error!(t, "baz-error");
        assert_eq!(t.get_last_error(), "baz-error");

        Ok(())
    }
}