palisade_errors/
logging.rs1use crate::ErrorCode;
20use std::borrow::Cow;
21use std::fmt;
22use zeroize::Zeroize;
23
24const MAX_FIELD_OUTPUT_LEN: usize = 1024;
26
27const TRUNCATION_INDICATOR: &str = "...[TRUNCATED]";
29
30#[derive(Debug)]
34pub struct ContextField {
35 value: Cow<'static, str>,
36}
37
38impl ContextField {
39 #[inline]
40 pub fn as_str(&self) -> &str {
41 self.value.as_ref()
42 }
43}
44
45impl From<&'static str> for ContextField {
46 fn from(value: &'static str) -> Self {
47 Self {
48 value: Cow::Borrowed(value),
49 }
50 }
51}
52
53impl From<String> for ContextField {
54 fn from(value: String) -> Self {
55 Self {
56 value: Cow::Owned(value),
57 }
58 }
59}
60
61impl From<Cow<'static, str>> for ContextField {
62 fn from(value: Cow<'static, str>) -> Self {
63 Self { value }
64 }
65}
66
67impl Zeroize for ContextField {
68 fn zeroize(&mut self) {
69 if let Cow::Owned(ref mut s) = self.value {
70 s.zeroize();
71 }
72 }
73}
74
75impl Drop for ContextField {
76 fn drop(&mut self) {
77 self.zeroize();
78 }
79}
80
81#[derive(Debug)]
96pub struct InternalLog<'a> {
97 pub code: &'a ErrorCode,
98 pub operation: &'a str,
99 pub details: &'a str,
100 pub source_internal: Option<&'a str>,
101 pub source_sensitive: Option<&'a str>,
102 pub metadata: &'a [(&'static str, ContextField)],
103 pub retryable: bool,
104}
105
106impl<'a> InternalLog<'a> {
107 #[cfg(all(feature = "trusted_debug", debug_assertions))]
123 pub fn format_for_trusted_debug(&self) -> String {
124 let mut output = format!(
125 "[{}] {} operation='{}' details='{}'",
126 self.code,
127 if self.retryable { "[RETRYABLE]" } else { "" },
128 truncate_with_indicator(self.operation),
129 truncate_with_indicator(self.details)
130 );
131
132 if let Some(internal) = self.source_internal {
133 output.push_str(&format!(
134 " source='{}'",
135 truncate_with_indicator(internal)
136 ));
137 }
138
139 if let Some(sensitive) = self.source_sensitive {
140 output.push_str(&format!(
141 " sensitive='{}'",
142 truncate_with_indicator(sensitive)
143 ));
144 }
145
146 for (key, value) in self.metadata {
147 output.push_str(&format!(
148 " {}='{}'",
149 key,
150 truncate_with_indicator(value.as_str())
151 ));
152 }
153
154 output
155 }
156
157 pub fn write_to(&self, f: &mut impl fmt::Write) -> fmt::Result {
173 write!(
174 f,
175 "[{}] {} operation='",
176 self.code,
177 if self.retryable { "[RETRYABLE]" } else { "" },
178 )?;
179 write_truncated(f, self.operation)?;
180 f.write_str("' details='")?;
181 write_truncated(f, self.details)?;
182 f.write_str("'")?;
183
184 if let Some(internal) = self.source_internal {
185 f.write_str(" source='")?;
186 write_truncated(f, internal)?;
187 f.write_str("'")?;
188 }
189
190 if let Some(sensitive) = self.source_sensitive {
191 f.write_str(" sensitive='")?;
192 write_truncated(f, sensitive)?;
193 f.write_str("'")?;
194 }
195
196 for (key, value) in self.metadata {
197 write!(f, " {}='", key)?;
198 write_truncated(f, value.as_str())?;
199 f.write_str("'")?;
200 }
201
202 Ok(())
203 }
204
205 #[inline]
213 pub const fn code(&self) -> &ErrorCode {
214 self.code
215 }
216
217 #[inline]
218 pub const fn operation(&self) -> &str {
219 self.operation
220 }
221
222 #[inline]
223 pub const fn details(&self) -> &str {
224 self.details
225 }
226
227 #[inline]
228 pub const fn source_internal(&self) -> Option<&str> {
229 self.source_internal
230 }
231
232 #[inline]
233 pub const fn source_sensitive(&self) -> Option<&str> {
234 self.source_sensitive
235 }
236
237 #[inline]
243 pub const fn metadata(&self) -> &[(&'static str, ContextField)] {
244 self.metadata
245 }
246
247 #[inline]
248 pub const fn is_retryable(&self) -> bool {
249 self.retryable
250 }
251}
252
253fn truncate_with_indicator(s: &str) -> Cow<'_, str> {
260 if s.len() <= MAX_FIELD_OUTPUT_LEN {
261 return Cow::Borrowed(s);
262 }
263
264 let max_content_len = MAX_FIELD_OUTPUT_LEN.saturating_sub(TRUNCATION_INDICATOR.len());
266
267 let mut idx = max_content_len;
269 while idx > 0 && !s.is_char_boundary(idx) {
270 idx -= 1;
271 }
272
273 if idx == 0 {
275 return Cow::Borrowed(TRUNCATION_INDICATOR);
276 }
277
278 let mut result = String::with_capacity(idx + TRUNCATION_INDICATOR.len());
280 result.push_str(&s[..idx]);
281 result.push_str(TRUNCATION_INDICATOR);
282 Cow::Owned(result)
283}
284
285#[inline]
286fn write_truncated(f: &mut impl fmt::Write, s: &str) -> fmt::Result {
287 if s.len() <= MAX_FIELD_OUTPUT_LEN {
288 return f.write_str(s);
289 }
290
291 let max_content_len = MAX_FIELD_OUTPUT_LEN.saturating_sub(TRUNCATION_INDICATOR.len());
292 let mut idx = max_content_len;
293 while idx > 0 && !s.is_char_boundary(idx) {
294 idx -= 1;
295 }
296
297 if idx == 0 {
298 return f.write_str(TRUNCATION_INDICATOR);
299 }
300
301 f.write_str(&s[..idx])?;
302 f.write_str(TRUNCATION_INDICATOR)
303}
304
305#[cfg(test)]
306mod tests {
307 use super::*;
308
309 #[test]
310 fn truncate_ascii() {
311 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN + 10);
312 let truncated = truncate_with_indicator(&s);
313
314 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
316
317 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
319 }
320
321 #[test]
322 fn no_truncate_when_under_limit() {
323 let s = "short string";
324 let truncated = truncate_with_indicator(s);
325
326 assert!(matches!(truncated, Cow::Borrowed(_)));
328 assert_eq!(truncated, s);
329 }
330
331 #[test]
332 fn truncate_utf8_boundary() {
333 let s = "й".repeat(MAX_FIELD_OUTPUT_LEN); let truncated = truncate_with_indicator(&s);
336
337 let _ = truncated.to_string();
339
340 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
342
343 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
345 }
346
347 #[test]
348 fn truncate_emoji() {
349 let s = "🔥".repeat(MAX_FIELD_OUTPUT_LEN); let truncated = truncate_with_indicator(&s);
351
352 assert!(std::str::from_utf8(truncated.as_bytes()).is_ok());
354
355 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
357 }
358
359 #[test]
360 fn exactly_at_limit() {
361 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN);
362 let truncated = truncate_with_indicator(&s);
363
364 assert!(matches!(truncated, Cow::Borrowed(_)));
366 assert_eq!(truncated.len(), MAX_FIELD_OUTPUT_LEN);
367 assert!(!truncated.ends_with(TRUNCATION_INDICATOR));
368 }
369
370 #[test]
371 fn one_over_limit() {
372 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN + 1);
373 let truncated = truncate_with_indicator(&s);
374
375 assert!(matches!(truncated, Cow::Owned(_)));
377 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
378 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
379 }
380
381 #[test]
382 fn context_field_zeroizes_owned() {
383 let mut field = ContextField::from(String::from("sensitive"));
384
385 assert!(matches!(field.value, Cow::Owned(_)));
387
388 field.zeroize();
390
391 assert_eq!(field.as_str(), "");
393 }
394
395 #[test]
396 fn context_field_doesnt_zeroize_borrowed() {
397 let mut field = ContextField::from("static");
398
399 assert!(matches!(field.value, Cow::Borrowed(_)));
401
402 field.zeroize();
404
405 assert_eq!(field.as_str(), "static");
407 }
408}