fedimint_core/
fmt_utils.rs

1use std::{cmp, fmt, ops, thread_local};
2
3use serde_json::Value;
4
5pub fn rust_log_full_enabled() -> bool {
6    // this will be called only once per-thread for best performance
7    thread_local!(static RUST_LOG_FULL: bool = {
8        std::env::var_os("RUST_LOG_FULL").is_some_and(|val| !val.is_empty())
9    });
10    RUST_LOG_FULL.with(|x| *x)
11}
12
13/// Optional stacktrace formatting for errors.
14///
15/// Automatically use `Display` (no stacktrace) or `Debug` depending on
16/// `RUST_LOG_FULL` env variable.
17///
18/// Meant for logging errors.
19pub struct OptStacktrace<T>(pub T);
20
21impl<T> fmt::Display for OptStacktrace<T>
22where
23    T: fmt::Debug + fmt::Display,
24{
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        if rust_log_full_enabled() {
27            fmt::Debug::fmt(&self.0, f)
28        } else {
29            fmt::Display::fmt(&self.0, f)
30        }
31    }
32}
33
34/// Use for displaying bytes in the logs
35///
36/// Will truncate values longer than 64 bytes, unless `RUST_LOG_FULL`
37/// environment variable is set to a non-empty value.
38pub struct AbbreviateHexBytes<'a>(pub &'a [u8]);
39
40impl<'a> fmt::Display for AbbreviateHexBytes<'a> {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        if self.0.len() <= 64 || rust_log_full_enabled() {
43            fedimint_core::format_hex(self.0, f)?;
44        } else {
45            fedimint_core::format_hex(&self.0[..64], f)?;
46            f.write_fmt(format_args!("-{}", self.0.len()))?;
47        }
48        Ok(())
49    }
50}
51
52impl<'a> fmt::Debug for AbbreviateHexBytes<'a> {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        fmt::Display::fmt(self, f)
55    }
56}
57
58/// Use for displaying potentially large `[serde_json::Value]`s in the logs
59///
60/// Notably, unlike normal `fmt::Debug` for `serde_json::Value` it does not
61/// respect pretty-printing and other formatting settings on the `formatter`.
62/// Which for debugging & logs should be OK.
63pub struct AbbreviateJson<'a>(pub &'a serde_json::Value);
64
65// TODO: use `str::floor_char_boundary` instead (when it becomes stable)
66// https://github.com/rust-lang/rust/blob/97872b792c9dd6a9bc5c3f4e62a0bd5958b09cdc/library/core/src/str/mod.rs#L258
67pub fn floor_char_boundary(s: &str, index: usize) -> usize {
68    // https://github.com/rust-lang/rust/blob/97872b792c9dd6a9bc5c3f4e62a0bd5958b09cdc/library/core/src/num/mod.rs#L883
69    #[inline]
70    pub const fn is_utf8_char_boundary(byte: u8) -> bool {
71        // This is bit magic equivalent to: b < 128 || b >= 192
72        (byte as i8) >= -0x40
73    }
74
75    if index >= s.len() {
76        s.len()
77    } else {
78        let lower_bound = index.saturating_sub(3);
79        let new_index = s.as_bytes()[lower_bound..=index]
80            .iter()
81            .rposition(|b| is_utf8_char_boundary(*b));
82
83        // SAFETY: we know that the character boundary will be within four bytes
84        unsafe { lower_bound + new_index.unwrap_unchecked() }
85    }
86}
87
88/// Format json string value if it's too long
89fn fmt_abbreviated_str(value: &str, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
90    const STRING_ABBR_LEN: usize = 128;
91    fmt::Debug::fmt(
92        &value[..floor_char_boundary(value, cmp::min(STRING_ABBR_LEN, value.len()))],
93        formatter,
94    )?;
95    if STRING_ABBR_LEN < value.len() {
96        formatter.write_fmt(format_args!("... {} total", value.len()))?;
97    }
98    Ok(())
99}
100
101/// Format json array value truncating elements if there's too many, and values
102/// if they are too long
103fn fmt_abbreviated_vec(vec: &[Value], formatter: &mut fmt::Formatter) -> fmt::Result {
104    const ARRAY_ABBR_LEN: usize = 64;
105    formatter.write_str("[")?;
106    for (i, v) in vec.iter().enumerate().take(ARRAY_ABBR_LEN) {
107        fmt::Debug::fmt(&AbbreviateJson(v), formatter)?;
108        if i != vec.len() - 1 {
109            formatter.write_str(", ")?;
110        }
111    }
112    if ARRAY_ABBR_LEN < vec.len() {
113        formatter.write_fmt(format_args!("... {} total", vec.len()))?;
114    }
115    formatter.write_str("]")?;
116    Ok(())
117}
118
119/// Format json object value truncating keys if there's too many, and keys and
120/// values if they are too long
121fn fmt_abbreviated_object(
122    map: &serde_json::Map<String, Value>,
123    formatter: &mut fmt::Formatter,
124) -> fmt::Result {
125    const MAP_ABBR_LEN: usize = 64;
126    formatter.write_str("{")?;
127    for (i, (k, v)) in map.iter().enumerate().take(MAP_ABBR_LEN) {
128        fmt_abbreviated_str(k, formatter)?;
129        formatter.write_str(": ")?;
130        fmt::Debug::fmt(&AbbreviateJson(v), formatter)?;
131        if i != map.len() - 1 {
132            formatter.write_str(", ")?;
133        }
134    }
135    if MAP_ABBR_LEN < map.len() {
136        formatter.write_fmt(format_args!("... {} total", map.len()))?;
137    }
138    formatter.write_str("}")?;
139    Ok(())
140}
141
142impl<'a> fmt::Debug for AbbreviateJson<'a> {
143    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
144        if rust_log_full_enabled() {
145            std::fmt::Debug::fmt(&self.0, formatter)
146        } else {
147            // modified https://github.com/serde-rs/json/blob/e41ee42d92022dbffc00f4ed50580fa5e060a379/src/value/mod.rs#L177
148            match self.0 {
149                Value::Null => formatter.write_str("Null"),
150                Value::Bool(boolean) => write!(formatter, "Bool({boolean})"),
151                Value::Number(number) => fmt::Debug::fmt(number, formatter),
152                Value::String(string) => {
153                    formatter.write_str("String(")?;
154                    fmt_abbreviated_str(string, formatter)?;
155                    formatter.write_str(")")
156                }
157                Value::Array(vec) => {
158                    formatter.write_str("Array ")?;
159                    fmt_abbreviated_vec(vec, formatter)
160                }
161                Value::Object(map) => {
162                    formatter.write_str("Object ")?;
163                    fmt_abbreviated_object(map, formatter)
164                }
165            }
166        }
167    }
168}
169
170/// Something that can be debug-formatted in an abbreviated way
171pub trait AbbreviatedDebug {
172    fn abbreviated_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result;
173}
174
175/// A wrapper that causes the inner `T` to be debug-formatted using
176/// [`AbbreviatedDebug`]
177///
178/// Useful in situations where using more specific wrapper is not feasible,
179/// e.g. the value to be abbreviated is nested inside larger struct
180/// where everything should be `debug-printed` together.
181pub struct AbbreviateDebug<T>(pub T);
182
183impl<T> fmt::Debug for AbbreviateDebug<T>
184where
185    T: AbbreviatedDebug,
186{
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        self.0.abbreviated_fmt(f)
189    }
190}
191
192impl<T> ops::Deref for AbbreviateDebug<T> {
193    type Target = T;
194
195    fn deref(&self) -> &Self::Target {
196        &self.0
197    }
198}
199
200impl AbbreviatedDebug for serde_json::Value {
201    fn abbreviated_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        fmt::Debug::fmt(&AbbreviateJson(self), f)
203    }
204}
205
206impl<const N: usize> AbbreviatedDebug for [u8; N] {
207    fn abbreviated_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
208        fmt::Debug::fmt(&AbbreviateHexBytes(self), f)
209    }
210}
211
212impl AbbreviatedDebug for &[serde_json::Value] {
213    fn abbreviated_fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
214        fmt_abbreviated_vec(self, f)
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    #[test]
223    fn sanity_check_abbreviate_json() {
224        for v in [
225            serde_json::json!(null),
226            serde_json::json!(true),
227            serde_json::json!(false),
228            serde_json::json!("foo"),
229            serde_json::json!({}),
230            serde_json::json!([]),
231            serde_json::json!([1]),
232            serde_json::json!([1, 3, 4]),
233            serde_json::json!({"a": "b"}),
234            serde_json::json!({"a": "b", "c": "d"}),
235            serde_json::json!({"a": { "foo": "bar"}, "c": "d"}),
236            serde_json::json!({"a": [1, 2, 3, 4], "b": {"c": "d"}}),
237            serde_json::json!([{"a": "b"}]),
238            serde_json::json!([{"a": "b"}, {"d": "f"}]),
239            serde_json::json!([null]),
240        ] {
241            assert_eq!(format!("{:?}", &v), format!("{:?}", AbbreviateJson(&v)));
242        }
243    }
244}