1use std::io::{self, Write};
4
5pub const SHADOW_LOG_KEY_MAX_BYTES: usize = 2 * 1024;
7pub const SHADOW_LOG_VALUE_MAX_BYTES: usize = 8 * 1024;
9pub const SHADOW_LOG_DIFF_MAX_BYTES: usize = 8 * 1024;
11pub const SHADOW_MISMATCH_MAX_ENTRIES: usize = 64;
14pub const SHADOW_MISMATCH_PATH_MAX_BYTES: usize = 512;
16pub const SHADOW_MISMATCH_SIDE_MAX_BYTES: usize = 1024;
19pub const SHADOW_MISMATCH_MAX_DEPTH: usize = 32;
21pub const SHADOW_LOG_TRUNCATION_MARKER: &str = "...[truncated]";
23
24pub(crate) struct JsonPrefix {
27 bytes: Vec<u8>,
28 truncated: bool,
29}
30
31impl JsonPrefix {
32 pub(crate) fn new() -> Self {
33 Self {
34 bytes: Vec::with_capacity(SHADOW_LOG_VALUE_MAX_BYTES),
35 truncated: false,
36 }
37 }
38
39 pub(crate) fn finish(mut self) -> String {
40 if self.truncated {
41 self.bytes
42 .truncate(SHADOW_LOG_VALUE_MAX_BYTES - SHADOW_LOG_TRUNCATION_MARKER.len());
43 }
44 if let Err(error) = std::str::from_utf8(&self.bytes) {
47 self.bytes.truncate(error.valid_up_to());
48 }
49 let mut text = String::from_utf8(self.bytes).expect("valid JSON prefix");
50 if self.truncated {
51 text.push_str(SHADOW_LOG_TRUNCATION_MARKER);
52 }
53 text
54 }
55}
56
57impl Write for JsonPrefix {
58 fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
59 let retained = bytes
60 .len()
61 .min(SHADOW_LOG_VALUE_MAX_BYTES - self.bytes.len());
62 self.bytes.extend_from_slice(&bytes[..retained]);
63 self.truncated |= retained < bytes.len();
64 Ok(bytes.len())
65 }
66
67 fn flush(&mut self) -> io::Result<()> {
68 Ok(())
69 }
70}
71
72pub(crate) fn json_preview<T: serde::Serialize>(value: &T) -> Option<String> {
75 let mut prefix = JsonPrefix::new();
76 serde_json::to_writer(&mut prefix, value).ok()?;
77 Some(prefix.finish())
78}
79
80pub fn clamp_utf8(value: &str, max_bytes: usize) -> String {
83 if value.len() <= max_bytes {
84 return value.to_string();
85 }
86 let mut limit = max_bytes.saturating_sub(SHADOW_LOG_TRUNCATION_MARKER.len());
87 while limit > 0 && !value.is_char_boundary(limit) {
88 limit -= 1;
89 }
90 let mut result = String::with_capacity(limit + SHADOW_LOG_TRUNCATION_MARKER.len());
91 result.push_str(&value[..limit]);
92 result.push_str(SHADOW_LOG_TRUNCATION_MARKER);
93 result
94}
95
96pub fn preview_key(key: &str) -> String {
98 clamp_utf8(key, SHADOW_LOG_KEY_MAX_BYTES)
99}
100
101pub fn preview_value(json: &str) -> String {
103 clamp_utf8(json, SHADOW_LOG_VALUE_MAX_BYTES)
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use serde::ser::{Error, SerializeSeq};
110 use serde::Serialize;
111
112 #[test]
113 fn clamps_on_character_boundaries() {
114 let text = "é".repeat(2000);
115 let clamped = clamp_utf8(&text, 100);
116 assert!(clamped.len() <= 100);
117 assert!(clamped.ends_with(SHADOW_LOG_TRUNCATION_MARKER));
118 assert_eq!(clamp_utf8("short", 100), "short");
119 }
120
121 #[test]
122 fn streamed_json_preserves_the_existing_preview_prefix() {
123 for value in [
124 "short".to_owned(),
125 "x".repeat(SHADOW_LOG_VALUE_MAX_BYTES - 2),
126 "x".repeat(SHADOW_LOG_VALUE_MAX_BYTES - 1),
127 "é🙂".repeat(SHADOW_LOG_VALUE_MAX_BYTES),
128 "\n\t\"\\".repeat(SHADOW_LOG_VALUE_MAX_BYTES),
129 ] {
130 assert_eq!(
131 json_preview(&value).unwrap(),
132 preview_value(&serde_json::to_string(&value).unwrap())
133 );
134 }
135 }
136
137 struct LargeSequence {
138 fail_at_end: bool,
139 }
140
141 impl Serialize for LargeSequence {
142 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
143 let mut sequence = serializer.serialize_seq(Some(10_000))?;
144 for _ in 0..10_000 {
145 sequence.serialize_element(&"large element".repeat(100))?;
146 }
147 if self.fail_at_end {
148 return Err(S::Error::custom("failure after the logged prefix"));
149 }
150 sequence.end()
151 }
152 }
153
154 #[test]
155 fn large_json_construction_keeps_a_capped_buffer_and_detects_late_errors() {
156 let mut prefix = JsonPrefix::new();
157 serde_json::to_writer(&mut prefix, &LargeSequence { fail_at_end: false }).unwrap();
158 assert_eq!(prefix.bytes.len(), SHADOW_LOG_VALUE_MAX_BYTES);
159 assert_eq!(prefix.bytes.capacity(), SHADOW_LOG_VALUE_MAX_BYTES);
160 assert!(prefix.finish().ends_with(SHADOW_LOG_TRUNCATION_MARKER));
161 assert!(json_preview(&LargeSequence { fail_at_end: true }).is_none());
162 }
163}