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='{}' details='{}'",
176 self.code,
177 if self.retryable { "[RETRYABLE]" } else { "" },
178 truncate_with_indicator(self.operation),
179 truncate_with_indicator(self.details)
180 )?;
181
182 if let Some(internal) = self.source_internal {
183 write!(f, " source='{}'", truncate_with_indicator(internal))?;
184 }
185
186 if let Some(sensitive) = self.source_sensitive {
187 write!(f, " sensitive='{}'", truncate_with_indicator(sensitive))?;
188 }
189
190 for (key, value) in self.metadata {
191 write!(
192 f,
193 " {}='{}'",
194 key,
195 truncate_with_indicator(value.as_str())
196 )?;
197 }
198
199 Ok(())
200 }
201
202 #[inline]
210 pub const fn code(&self) -> &ErrorCode {
211 self.code
212 }
213
214 #[inline]
215 pub const fn operation(&self) -> &str {
216 self.operation
217 }
218
219 #[inline]
220 pub const fn details(&self) -> &str {
221 self.details
222 }
223
224 #[inline]
225 pub const fn source_internal(&self) -> Option<&str> {
226 self.source_internal
227 }
228
229 #[inline]
230 pub const fn source_sensitive(&self) -> Option<&str> {
231 self.source_sensitive
232 }
233
234 #[inline]
240 pub const fn metadata(&self) -> &[(&'static str, ContextField)] {
241 self.metadata
242 }
243
244 #[inline]
245 pub const fn is_retryable(&self) -> bool {
246 self.retryable
247 }
248}
249
250fn truncate_with_indicator(s: &str) -> Cow<'_, str> {
257 if s.len() <= MAX_FIELD_OUTPUT_LEN {
258 return Cow::Borrowed(s);
259 }
260
261 let max_content_len = MAX_FIELD_OUTPUT_LEN.saturating_sub(TRUNCATION_INDICATOR.len());
263
264 let mut idx = max_content_len;
266 while idx > 0 && !s.is_char_boundary(idx) {
267 idx -= 1;
268 }
269
270 if idx == 0 {
272 return Cow::Borrowed(TRUNCATION_INDICATOR);
273 }
274
275 let mut result = String::with_capacity(idx + TRUNCATION_INDICATOR.len());
277 result.push_str(&s[..idx]);
278 result.push_str(TRUNCATION_INDICATOR);
279 Cow::Owned(result)
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn truncate_ascii() {
288 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN + 10);
289 let truncated = truncate_with_indicator(&s);
290
291 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
293
294 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
296 }
297
298 #[test]
299 fn no_truncate_when_under_limit() {
300 let s = "short string";
301 let truncated = truncate_with_indicator(s);
302
303 assert!(matches!(truncated, Cow::Borrowed(_)));
305 assert_eq!(truncated, s);
306 }
307
308 #[test]
309 fn truncate_utf8_boundary() {
310 let s = "й".repeat(MAX_FIELD_OUTPUT_LEN); let truncated = truncate_with_indicator(&s);
313
314 let _ = truncated.to_string();
316
317 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
319
320 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
322 }
323
324 #[test]
325 fn truncate_emoji() {
326 let s = "🔥".repeat(MAX_FIELD_OUTPUT_LEN); let truncated = truncate_with_indicator(&s);
328
329 assert!(std::str::from_utf8(truncated.as_bytes()).is_ok());
331
332 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
334 }
335
336 #[test]
337 fn exactly_at_limit() {
338 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN);
339 let truncated = truncate_with_indicator(&s);
340
341 assert!(matches!(truncated, Cow::Borrowed(_)));
343 assert_eq!(truncated.len(), MAX_FIELD_OUTPUT_LEN);
344 assert!(!truncated.ends_with(TRUNCATION_INDICATOR));
345 }
346
347 #[test]
348 fn one_over_limit() {
349 let s = "a".repeat(MAX_FIELD_OUTPUT_LEN + 1);
350 let truncated = truncate_with_indicator(&s);
351
352 assert!(matches!(truncated, Cow::Owned(_)));
354 assert!(truncated.len() <= MAX_FIELD_OUTPUT_LEN);
355 assert!(truncated.ends_with(TRUNCATION_INDICATOR));
356 }
357
358 #[test]
359 fn context_field_zeroizes_owned() {
360 let mut field = ContextField::from(String::from("sensitive"));
361
362 assert!(matches!(field.value, Cow::Owned(_)));
364
365 field.zeroize();
367
368 assert_eq!(field.as_str(), "");
370 }
371
372 #[test]
373 fn context_field_doesnt_zeroize_borrowed() {
374 let mut field = ContextField::from("static");
375
376 assert!(matches!(field.value, Cow::Borrowed(_)));
378
379 field.zeroize();
381
382 assert_eq!(field.as_str(), "static");
384 }
385}