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