mysten_common/
logging.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::in_test_configuration;
5use once_cell::sync::Lazy;
6
7#[macro_export]
8macro_rules! fatal {
9    ($msg:literal $(, $arg:expr)*) => {{
10        if $crate::in_antithesis() {
11            let full_msg = format!($msg $(, $arg)*);
12            let json = $crate::logging::json!({ "message": full_msg });
13            $crate::logging::assert_unreachable_antithesis!($msg, &json);
14        }
15        tracing::error!(fatal = true, $msg $(, $arg)*);
16        panic!($msg $(, $arg)*);
17    }};
18}
19
20pub use antithesis_sdk::assert_reachable as assert_reachable_antithesis;
21pub use antithesis_sdk::assert_sometimes as assert_sometimes_antithesis;
22pub use antithesis_sdk::assert_unreachable as assert_unreachable_antithesis;
23
24pub use serde_json::json;
25
26#[inline(always)]
27pub fn crash_on_debug() -> bool {
28    static CRASH_ON_DEBUG: Lazy<bool> = Lazy::new(|| {
29        in_test_configuration() || std::env::var("SUI_ENABLE_DEBUG_ASSERTIONS").is_ok()
30    });
31
32    *CRASH_ON_DEBUG
33}
34
35#[cfg(msim)]
36pub mod intercept_debug_fatal {
37    use std::sync::{Arc, Mutex};
38
39    #[derive(Clone)]
40    pub struct DebugFatalCallback {
41        pub pattern: String,
42        pub callback: Arc<dyn Fn() + Send + Sync>,
43    }
44
45    thread_local! {
46        static INTERCEPT_DEBUG_FATAL: Mutex<Option<DebugFatalCallback>> = Mutex::new(None);
47    }
48
49    pub fn register_callback(message: &str, f: impl Fn() + Send + Sync + 'static) {
50        INTERCEPT_DEBUG_FATAL.with(|m| {
51            *m.lock().unwrap() = Some(DebugFatalCallback {
52                pattern: message.to_string(),
53                callback: Arc::new(f),
54            });
55        });
56    }
57
58    pub fn get_callback() -> Option<DebugFatalCallback> {
59        INTERCEPT_DEBUG_FATAL.with(|m| m.lock().unwrap().clone())
60    }
61}
62
63#[macro_export]
64macro_rules! register_debug_fatal_handler {
65    ($message:literal, $f:expr) => {
66        #[cfg(msim)]
67        $crate::logging::intercept_debug_fatal::register_callback($message, $f);
68
69        #[cfg(not(msim))]
70        {
71            // silence unused variable warnings from the body of the callback
72            let _ = $f;
73        }
74    };
75}
76
77/// Like `debug_fatal!`, but records `$location` (a `&str`) as the
78/// `system_invariant_violations` metric label instead of the macro's own
79/// `file!():line!()`. Use this when forwarding a caller-supplied location
80/// (e.g. from `#[track_caller]` + `Location::caller()`) so the metric points
81/// at the user's call site rather than the wrapper.
82#[macro_export]
83macro_rules! debug_fatal_at {
84    ($location:expr, $msg:literal $(, $arg:expr)*) => {{
85        loop {
86            #[cfg(msim)]
87            {
88                if let Some(cb) = $crate::logging::intercept_debug_fatal::get_callback() {
89                    tracing::error!($msg $(, $arg)*);
90                    let msg = format!($msg $(, $arg)*);
91                    if msg.contains(&cb.pattern) {
92                        (cb.callback)();
93                    }
94                    break;
95                }
96            }
97
98            // In antithesis, rather than crashing, we will use the assert_unreachable_antithesis
99            // macro to catch the signal that something has gone wrong.
100            if !$crate::in_antithesis() && $crate::logging::crash_on_debug() {
101                $crate::fatal!($msg $(, $arg)*);
102            } else {
103                let stacktrace = std::backtrace::Backtrace::capture();
104                tracing::error!(debug_fatal = true, stacktrace = ?stacktrace, $msg $(, $arg)*);
105                let location: &str = $location;
106                if let Some(metrics) = mysten_metrics::get_metrics() {
107                    metrics.system_invariant_violations.with_label_values(&[location]).inc();
108                }
109                if $crate::in_antithesis() {
110                    // antithesis requires a literal for first argument. pass the formatted argument
111                    // as a string.
112                    let full_msg = format!($msg $(, $arg)*);
113                    let json = $crate::logging::json!({ "message": full_msg });
114                    $crate::logging::assert_unreachable_antithesis!($msg, &json);
115                }
116            }
117            break;
118        }
119    }};
120}
121
122#[macro_export]
123macro_rules! debug_fatal {
124    //($msg:literal $(, $arg:expr)* $(,)?)
125    ($msg:literal $(, $arg:expr)*) => {{
126        $crate::debug_fatal_at!(concat!(file!(), ':', line!()), $msg $(, $arg)*);
127    }};
128}
129
130#[macro_export]
131macro_rules! debug_fatal_no_invariant {
132    ($msg:literal $(, $arg:expr)*) => {{
133        loop {
134            #[cfg(msim)]
135            {
136                if let Some(cb) = $crate::logging::intercept_debug_fatal::get_callback() {
137                    tracing::error!($msg $(, $arg)*);
138                    let msg = format!($msg $(, $arg)*);
139                    if msg.contains(&cb.pattern) {
140                        (cb.callback)();
141                    }
142                    break;
143                }
144            }
145
146            if !$crate::in_antithesis() && $crate::logging::crash_on_debug() {
147                $crate::fatal!($msg $(, $arg)*);
148            } else {
149                tracing::error!($msg $(, $arg)*);
150                if $crate::in_antithesis() {
151                    let full_msg = format!($msg $(, $arg)*);
152                    let json = $crate::logging::json!({ "message": full_msg });
153                    $crate::logging::assert_unreachable_antithesis!($msg, &json);
154                }
155            }
156            break;
157        }
158    }};
159}
160
161#[macro_export]
162macro_rules! assert_reachable {
163    () => {
164        $crate::logging::assert_reachable!("");
165    };
166    ($message:literal) => {{
167        // calling in to antithesis sdk breaks determinisim in simtests (on linux only)
168        if !cfg!(msim) {
169            $crate::logging::assert_reachable_antithesis!($message);
170        } else {
171            $crate::assert_reachable_simtest!($message);
172        }
173    }};
174}
175
176#[macro_export]
177macro_rules! assert_sometimes {
178    ($expr:expr, $message:literal) => {{
179        // calling in to antithesis sdk breaks determinisim in simtests (on linux only)
180        if !cfg!(msim) {
181            $crate::logging::assert_sometimes_antithesis!($expr, $message);
182        } else {
183            $crate::assert_sometimes_simtest!($expr, $message);
184        }
185    }};
186}
187
188mod tests {
189    #[test]
190    #[should_panic]
191    fn test_fatal() {
192        fatal!("This is a fatal error");
193    }
194
195    #[test]
196    #[should_panic]
197    fn test_debug_fatal() {
198        if cfg!(debug_assertions) {
199            debug_fatal!("This is a debug fatal error");
200        } else {
201            // pass in release mode as well
202            fatal!("This is a fatal error");
203        }
204    }
205
206    #[cfg(not(debug_assertions))]
207    #[test]
208    fn test_debug_fatal_release_mode() {
209        debug_fatal!("This is a debug fatal error");
210    }
211
212    #[test]
213    fn test_assert_sometimes_side_effects() {
214        let mut x = 0;
215
216        let mut inc = || {
217            x += 1;
218            true
219        };
220
221        assert_sometimes!(inc(), "");
222        assert_eq!(x, 1);
223    }
224}