cortex_ingest_core/
metadata.rs1use serde_json::{Map, Value};
4
5pub const MAX_METADATA_JSON_BYTES: usize = 64 * 1024;
7pub const MAX_METADATA_STRING_CHARS: usize = 2048;
9pub const MAX_METADATA_KEY_CHARS: usize = 128;
11pub const MAX_METADATA_OBJECT_FIELDS: usize = 128;
13
14const REDACTED: &str = "[REDACTED]";
15
16pub fn bounded_metadata_json(value: Value) -> String {
18 let value = sanitize_value(value, None);
19 let encoded = value.to_string();
20 if encoded.len() <= MAX_METADATA_JSON_BYTES {
21 return encoded;
22 }
23
24 let source_type = value
25 .get("source_type")
26 .and_then(Value::as_str)
27 .map(str::to_string);
28 serde_json::json!({
29 "source_type": source_type,
30 "metadata_truncated": true,
31 "metadata_original_bytes": encoded.len(),
32 })
33 .to_string()
34}
35
36pub fn try_bounded_metadata_json(value: Value) -> Option<String> {
41 let encoded = sanitize_value(value, None).to_string();
42 (encoded.len() <= MAX_METADATA_JSON_BYTES).then_some(encoded)
43}
44
45pub fn attrs_to_metadata_object<'a, I>(attrs: I) -> Value
47where
48 I: IntoIterator<Item = (&'a str, Value)>,
49{
50 let mut object = Map::new();
51 let mut omitted = 0usize;
52 for (key, value) in attrs {
53 if object.len() >= MAX_METADATA_OBJECT_FIELDS {
54 omitted += 1;
55 continue;
56 }
57 let stored_key = truncate_chars(key, MAX_METADATA_KEY_CHARS);
58 object.insert(stored_key, sanitize_value(value, Some(key)));
59 }
60 if omitted > 0 {
61 object.insert("_omitted_fields".to_string(), Value::Number(omitted.into()));
62 }
63 Value::Object(object)
64}
65
66fn sanitize_value(value: Value, key: Option<&str>) -> Value {
67 if key.is_some_and(is_sensitive_key) {
68 return Value::String(REDACTED.to_string());
69 }
70
71 match value {
72 Value::String(value) => Value::String(truncate_chars(&value, MAX_METADATA_STRING_CHARS)),
73 Value::Array(values) => Value::Array(
74 values
75 .into_iter()
76 .take(MAX_METADATA_OBJECT_FIELDS)
77 .map(|value| sanitize_value(value, key))
78 .collect(),
79 ),
80 Value::Object(values) => sanitize_object(values),
81 value => value,
82 }
83}
84
85fn sanitize_object(values: Map<String, Value>) -> Value {
86 let mut object = Map::new();
87 let mut omitted = 0usize;
88 for (key, value) in values {
89 if object.len() >= MAX_METADATA_OBJECT_FIELDS {
90 omitted += 1;
91 continue;
92 }
93 let stored_key = truncate_chars(&key, MAX_METADATA_KEY_CHARS);
94 object.insert(stored_key, sanitize_value(value, Some(&key)));
95 }
96 if omitted > 0 {
97 object.insert("_omitted_fields".to_string(), Value::Number(omitted.into()));
98 }
99 Value::Object(object)
100}
101
102fn is_sensitive_key(key: &str) -> bool {
103 let normalized = key.to_ascii_lowercase();
104 [
105 "authorization",
106 "bearer",
107 "cookie",
108 "credential",
109 "password",
110 "private_key",
111 "secret",
112 "set-cookie",
113 "token",
114 ]
115 .iter()
116 .any(|needle| normalized.contains(needle))
117 || normalized.contains("api_key")
118 || normalized.contains("apikey")
119 || normalized.contains("access_key")
120}
121
122fn truncate_chars(value: &str, max: usize) -> String {
123 let mut out = String::with_capacity(value.len().min(max));
124 for (idx, ch) in value.chars().enumerate() {
125 if idx >= max {
126 out.push_str("...[truncated]");
127 return out;
128 }
129 out.push(ch);
130 }
131 out
132}
133
134#[cfg(test)]
135#[path = "metadata_tests.rs"]
136mod tests;