1use crate::event::LogEvent;
14use serde::{Deserialize, Serialize};
15use std::collections::BTreeMap;
16use std::path::Path;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20pub enum Sensitivity {
21 Safe,
23 Diagnostic,
25 Sensitive,
27}
28
29#[derive(Debug, Clone, Default)]
31pub struct PathAliases {
32 pub app_root: Option<String>,
34 pub home: Option<String>,
36 pub temp: Option<String>,
38 pub data: Option<String>,
40 pub cache: Option<String>,
42}
43
44#[derive(Debug, Clone)]
46pub struct LogPolicy {
47 global: u8,
48 components: BTreeMap<String, u8>,
49 pub full_paths: bool,
51 pub sensitivity: Sensitivity,
53 pub paths: PathAliases,
55}
56
57impl Default for LogPolicy {
58 fn default() -> Self {
59 Self::new(crate::Verbosity::V4)
60 }
61}
62
63impl LogPolicy {
64 pub fn new(verbosity: crate::Verbosity) -> Self {
66 Self {
67 global: verbosity.value(),
68 components: BTreeMap::new(),
69 full_paths: false,
70 sensitivity: Sensitivity::Safe,
71 paths: PathAliases::default(),
72 }
73 }
74 pub fn set_component(&mut self, component: impl Into<String>, verbosity: crate::Verbosity) {
76 self.components.insert(component.into(), verbosity.value());
77 }
78 pub fn allows(&self, event: &LogEvent) -> bool {
80 self.allows_component(&event.component, event.verbosity)
81 }
82
83 pub(crate) fn allows_component(&self, component: &str, verbosity: crate::Verbosity) -> bool {
85 let configured = component_threshold(&self.components, component).unwrap_or(self.global);
86 verbosity.value() <= configured
87 }
88 pub fn sanitize(&self, event: &LogEvent) -> LogEvent {
95 self.sanitize_owned(event.clone())
96 }
97
98 pub fn sanitize_owned(&self, mut value: LogEvent) -> LogEvent {
100 if self.sensitivity != Sensitivity::Sensitive {
101 value.message = redact_text(value.message);
102 }
103 for field in &mut value.fields {
104 if field.sensitive {
105 field.value = "<REDACTED>".to_string();
106 } else if field.path && !self.full_paths && self.sensitivity != Sensitivity::Sensitive {
107 field.value = alias_path(&field.value, &self.paths);
108 }
109 }
110 value
111 }
112
113 pub const fn sensitive_warning() -> &'static str {
116 "Sensitive logging is active: encrypted diagnostics may contain confidential material."
117 }
118}
119
120fn component_threshold(components: &BTreeMap<String, u8>, component: &str) -> Option<u8> {
121 let mut candidate = component;
122 loop {
123 if let Some(verbosity) = components.get(candidate) {
124 return Some(*verbosity);
125 }
126 let (parent, _) = candidate.rsplit_once('.')?;
127 candidate = parent;
128 }
129}
130
131fn alias_path(value: &str, aliases: &PathAliases) -> String {
132 for (prefix, alias) in [
133 (&aliases.app_root, "<APP_ROOT>"),
134 (&aliases.home, "<HOME>"),
135 (&aliases.temp, "<TEMP>"),
136 (&aliases.cache, "<CACHE>"),
137 (&aliases.data, "<DATA>"),
138 ] {
139 if let Some(prefix) = prefix.as_deref() {
140 if Path::new(value).starts_with(prefix) {
141 return value.replacen(prefix, alias, 1);
142 }
143 }
144 }
145 "<LOCAL_PATH>".to_string()
146}
147
148fn redact_text(message: String) -> String {
149 if !message
150 .split_whitespace()
151 .any(|token| redaction_for_token(token).is_some())
152 {
153 return message;
154 }
155 let mut redacted = String::with_capacity(message.len());
156 for (index, token) in message.split_whitespace().enumerate() {
157 if index != 0 {
158 redacted.push(' ');
159 }
160 redacted.push_str(redaction_for_token(token).unwrap_or(token));
161 }
162 redacted
163}
164
165fn redaction_for_token(token: &str) -> Option<&'static str> {
166 let secret_markers = [
167 "token=",
168 "password=",
169 "secret=",
170 "api_key=",
171 "apikey=",
172 "authorization:",
173 "cookie:",
174 "set-cookie:",
175 "session=",
176 "private_key=",
177 ];
178 if secret_markers
179 .iter()
180 .any(|marker| contains_ascii_case_insensitive(token, marker))
181 {
182 return Some("<REDACTED>");
183 }
184 if token.contains("://") && token.contains('@') {
185 return Some("<REDACTED_URL>");
186 }
187 None
188}
189
190fn contains_ascii_case_insensitive(value: &str, pattern: &str) -> bool {
191 value.as_bytes().windows(pattern.len()).any(|window| {
192 window
193 .iter()
194 .zip(pattern.as_bytes())
195 .all(|(left, right)| left.eq_ignore_ascii_case(right))
196 })
197}