use crate::event::LogEvent;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Sensitivity {
Safe,
Diagnostic,
Sensitive,
}
#[derive(Debug, Clone, Default)]
pub struct PathAliases {
pub app_root: Option<String>,
pub home: Option<String>,
pub temp: Option<String>,
pub data: Option<String>,
pub cache: Option<String>,
}
#[derive(Debug, Clone)]
pub struct LogPolicy {
global: u8,
components: BTreeMap<String, u8>,
pub full_paths: bool,
pub sensitivity: Sensitivity,
pub paths: PathAliases,
}
impl Default for LogPolicy {
fn default() -> Self {
Self::new(crate::Verbosity::V4)
}
}
impl LogPolicy {
pub fn new(verbosity: crate::Verbosity) -> Self {
Self {
global: verbosity.value(),
components: BTreeMap::new(),
full_paths: false,
sensitivity: Sensitivity::Safe,
paths: PathAliases::default(),
}
}
pub fn set_component(&mut self, component: impl Into<String>, verbosity: crate::Verbosity) {
self.components.insert(component.into(), verbosity.value());
}
pub fn allows(&self, event: &LogEvent) -> bool {
self.allows_component(&event.component, event.verbosity)
}
pub(crate) fn allows_component(&self, component: &str, verbosity: crate::Verbosity) -> bool {
let configured = component_threshold(&self.components, component).unwrap_or(self.global);
verbosity.value() <= configured
}
pub fn sanitize(&self, event: &LogEvent) -> LogEvent {
self.sanitize_owned(event.clone())
}
pub fn sanitize_owned(&self, mut value: LogEvent) -> LogEvent {
if self.sensitivity != Sensitivity::Sensitive {
value.message = redact_text(value.message);
}
for field in &mut value.fields {
if field.sensitive {
field.value = "<REDACTED>".to_string();
} else if field.path && !self.full_paths && self.sensitivity != Sensitivity::Sensitive {
field.value = alias_path(&field.value, &self.paths);
}
}
value
}
pub const fn sensitive_warning() -> &'static str {
"Sensitive logging is active: encrypted diagnostics may contain confidential material."
}
}
fn component_threshold(components: &BTreeMap<String, u8>, component: &str) -> Option<u8> {
let mut candidate = component;
loop {
if let Some(verbosity) = components.get(candidate) {
return Some(*verbosity);
}
let (parent, _) = candidate.rsplit_once('.')?;
candidate = parent;
}
}
fn alias_path(value: &str, aliases: &PathAliases) -> String {
for (prefix, alias) in [
(&aliases.app_root, "<APP_ROOT>"),
(&aliases.home, "<HOME>"),
(&aliases.temp, "<TEMP>"),
(&aliases.cache, "<CACHE>"),
(&aliases.data, "<DATA>"),
] {
if let Some(prefix) = prefix.as_deref() {
if Path::new(value).starts_with(prefix) {
return value.replacen(prefix, alias, 1);
}
}
}
"<LOCAL_PATH>".to_string()
}
fn redact_text(message: String) -> String {
if !message
.split_whitespace()
.any(|token| redaction_for_token(token).is_some())
{
return message;
}
let mut redacted = String::with_capacity(message.len());
for (index, token) in message.split_whitespace().enumerate() {
if index != 0 {
redacted.push(' ');
}
redacted.push_str(redaction_for_token(token).unwrap_or(token));
}
redacted
}
fn redaction_for_token(token: &str) -> Option<&'static str> {
let secret_markers = [
"token=",
"password=",
"secret=",
"api_key=",
"apikey=",
"authorization:",
"cookie:",
"set-cookie:",
"session=",
"private_key=",
];
if secret_markers
.iter()
.any(|marker| contains_ascii_case_insensitive(token, marker))
{
return Some("<REDACTED>");
}
if token.contains("://") && token.contains('@') {
return Some("<REDACTED_URL>");
}
None
}
fn contains_ascii_case_insensitive(value: &str, pattern: &str) -> bool {
value.as_bytes().windows(pattern.len()).any(|window| {
window
.iter()
.zip(pattern.as_bytes())
.all(|(left, right)| left.eq_ignore_ascii_case(right))
})
}