impl IoWrite for RedactingWriter { fn write(&mut self, bytes: &[u8]) -> io::Result { self.buffer.extend_from_slice(bytes); Ok(bytes.len()) } fn flush(&mut self) -> io::Result<()> { if self.buffer.is_empty() { return Ok(()); } let text = String::from_utf8_lossy(&self.buffer); let redacted = redact_text(&text); let mut stderr = io::stderr().lock(); stderr.write_all(redacted.as_bytes())?; stderr.flush()?; self.buffer.clear(); Ok(()) } } impl Drop for RedactingWriter { fn drop(&mut self) { let _ = self.flush(); } } impl<'a> tracing_subscriber::fmt::MakeWriter<'a> for RedactingStderr { type Writer = RedactingWriter; fn make_writer(&'a self) -> Self::Writer { RedactingWriter { buffer: Vec::with_capacity(512), } } } fn redact_regex(pattern: &'static str) -> &'static regex::Regex { static AUTH: OnceLock = OnceLock::new(); static QUERY: OnceLock = OnceLock::new(); static JSON: OnceLock = OnceLock::new(); static KV: OnceLock = OnceLock::new(); static PEM: OnceLock = OnceLock::new(); match pattern { "auth" => AUTH.get_or_init(|| regex::Regex::new(r"(?i)(authorization\s*[:=]\s*)(?:bearer\s+)?[^\s,;]+" ).expect("redaction regex")), "query" => QUERY.get_or_init(|| regex::Regex::new(r"(?i)([?&](?:token|access_token|api_token|password|backup_password|secret|credential|credentials|key_secret|application_secret|consumer_key)=)[^&\s]+" ).expect("redaction regex")), "json" => JSON.get_or_init(|| regex::Regex::new(r#"(?i)(\"(?:authorization|proxy_authorization|api_token|access_token|application_secret|consumer_key|key_secret|backup_password|password|credential|credentials|secret|token|private_key|master_key)\"\s*:\s*\")[^\"]*(\")"#).expect("redaction regex")), "kv" => KV.get_or_init(|| regex::Regex::new(r#"(?i)((?:authorization|proxy[_-]?authorization|api[_-]?token|access[_-]?token|application[_-]?secret|consumer[_-]?key|key[_-]?secret|backup[_-]?password|password|credential|credentials|secret|token|private[_-]?key|master[_-]?key)\s*[=:]\s*)[^\s,;"']+"#).expect("redaction regex")), "pem" => PEM.get_or_init(|| regex::Regex::new(r"(?s)-----BEGIN [^-\r\n]*PRIVATE KEY-----.*?-----END [^-\r\n]*PRIVATE KEY-----" ).expect("redaction regex")), _ => unreachable!(), } } fn is_secret_field(name: &str) -> bool { matches!( name.to_ascii_lowercase().as_str(), "authorization" | "proxy_authorization" | "api_token" | "access_token" | "application_secret" | "consumer_key" | "key_secret" | "backup_password" | "password" | "credential" | "credentials" | "secret" | "token" | "private_key" | "master_key" ) } pub fn redact_text(input: &str) -> String { let mut value = redact_regex("pem") .replace_all(input, "[REDACTED PRIVATE KEY]") .into_owned(); value = redact_regex("auth") .replace_all(&value, "$1[REDACTED]") .into_owned(); value = redact_regex("query") .replace_all(&value, "$1[REDACTED]") .into_owned(); value = redact_regex("json") .replace_all(&value, "$1[REDACTED]$2") .into_owned(); redact_regex("kv") .replace_all(&value, "$1[REDACTED]") .into_owned() } fn redact_value(value: &mut Value) { match value { Value::String(text) => *text = redact_text(text), Value::Array(items) => { for item in items { redact_value(item); } } Value::Object(items) => { for (key, item) in items { if is_secret_field(key) { *item = Value::String("[REDACTED]".into()); } else { redact_value(item); } } } _ => {} } } fn rfc3339_now() -> String { let ms = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or_default() .as_millis() as i64; let seconds = ms / 1000; let millis = ms % 1000; let days = seconds.div_euclid(86_400); let day_seconds = seconds.rem_euclid(86_400); let z = days + 719_468; let era = if z >= 0 { z } else { z - 146_096 }.div_euclid(146_097); let doe = z - era * 146_097; let yoe = (doe - doe / 1_460 + doe / 36_524 - doe / 146_096) / 365; let mut year = yoe + era * 400; let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); let mp = (5 * doy + 2) / 153; let day = doy - (153 * mp + 2) / 5 + 1; let month = mp + if mp < 10 { 3 } else { -9 }; if month <= 2 { year += 1; } let hour = day_seconds / 3600; let minute = (day_seconds % 3600) / 60; let second = day_seconds % 60; format!("{year:04}-{month:02}-{day:02}T{hour:02}:{minute:02}:{second:02}.{millis:03}Z") } async fn open_error_file(path: PathBuf) -> Result { let file = tokio::task::spawn_blocking(move || -> Result { let mut options = std::fs::OpenOptions::new(); options.create(true).append(true).write(true); #[cfg(unix)] { use std::os::unix::fs::OpenOptionsExt; options.mode(0o600).custom_flags(libc::O_NOFOLLOW); } let file = options.open(&path)?; let metadata = file.metadata()?; ensure!(metadata.is_file(), "error log path is not a regular file"); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; file.set_permissions(std::fs::Permissions::from_mode(0o600))?; } Ok(file) }) .await .map_err(|_| anyhow::anyhow!("error log open worker failed"))??; Ok(File::from_std(file)) } fn format_error_event(event: &ErrorEvent, format: ErrorLogFormat) -> Vec { match format { ErrorLogFormat::Json => { let mut object = Map::new(); object.insert("timestamp".into(), Value::String(event.timestamp.clone())); object.insert("level".into(), Value::String("ERROR".into())); object.insert("target".into(), Value::String(event.target.clone())); object.insert("message".into(), Value::String(event.message.clone())); for (key, value) in &event.fields { if !matches!(key.as_str(), "timestamp" | "level" | "target" | "message") { object.insert(key.clone(), Value::String(value.clone())); } } let mut line = serde_json::to_vec(&Value::Object(object)).unwrap_or_default(); line.push(b'\n'); line } ErrorLogFormat::Raw => { let one_line = |value: &str| value.replace('\r', "\\r").replace('\n', "\\n"); let mut line = format!( "{} ERROR {} {}", event.timestamp, event.target, one_line(&event.message) ); for (key, value) in &event.fields { line.push(' '); line.push_str(key); line.push('='); line.push_str(&one_line(value)); } line.push('\n'); line.into_bytes() } } } async fn error_log_loop( mut receiver: mpsc::Receiver, settings: Arc>, mut settings_changed: watch::Receiver<()>, written: Arc, errors: Arc, ) { let mut open_path: Option = None; let mut file: Option = None; let mut retry_after: Option = None; let mut last_warning: Option = None; loop { let event = tokio::select! { event = receiver.recv() => match event { Some(event) => event, None => break, }, changed = settings_changed.changed() => { if changed.is_err() && receiver.is_closed() { break; } let config = settings.load_full(); let path = PathBuf::from(&config.path); if !config.enabled || open_path.as_ref() != Some(&path) { // Dropping Tokio's File closes the descriptor without blocking // the request path. A changed path is reopened on the next event. file = None; open_path = None; retry_after = None; } continue; } }; let config = settings.load_full(); if !config.enabled { file = None; open_path = None; retry_after = None; continue; } let path = PathBuf::from(&config.path); if open_path.as_ref() != Some(&path) { file = None; open_path = Some(path.clone()); retry_after = None; } if file.is_none() && retry_after.is_none_or(|until| Instant::now() >= until) { match open_error_file(path.clone()).await { Ok(opened) => { file = Some(opened); retry_after = None; } Err(error) => { errors.fetch_add(1, Ordering::Relaxed); retry_after = Some(Instant::now() + ERROR_LOG_RETRY); if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) { tracing::warn!(path=%path.display(), %error, "optional error log file is unavailable"); last_warning = Some(Instant::now()); } } } } let Some(opened) = file.as_mut() else { continue; }; let line = format_error_event(&event, config.format); if let Err(error) = opened.write_all(&line).await { errors.fetch_add(1, Ordering::Relaxed); file = None; retry_after = Some(Instant::now() + ERROR_LOG_RETRY); if last_warning.is_none_or(|time| time.elapsed() >= ERROR_LOG_RETRY) { tracing::warn!(path=%path.display(), %error, "optional error log write failed"); last_warning = Some(Instant::now()); } } else { written.fetch_add(1, Ordering::Relaxed); } } }