//! Identifier-suppressing tracing layer driven by `[server] no_logs` / `[client] no_logs`. //! //! ## Motivation //! //! Russian telecom regulations now require operators to forward identifying customer data //! (passport / INN / IP / domain / logins / geolocation) on request. To keep an Aura node from //! becoming a treasure-trove of those exact fields in its own logs, `no_logs = true` swaps the //! default tracing formatter for one that skips writing a configured list of "identifier" fields //! to the log line. The event still fires (counters and rates are unaffected), but the offending //! field is rendered as nothing in the formatted output. //! //! ## Mechanism //! //! [`init_filtered_tracing`] installs a `tracing-subscriber::fmt` subscriber whose //! [`FormatFields`](tracing_subscriber::fmt::FormatFields) is a custom //! [`debug_fn`](tracing_subscriber::fmt::format::debug_fn) closure. The closure inspects //! [`Field::name`](tracing::field::Field::name) against the [`REDACTED_FIELD_NAMES`] blacklist; on //! a match it writes nothing (not even the field name), so the resulting log line literally //! contains no token from the redacted value. Non-blacklisted fields go through the standard //! `"key=value "` formatting. //! //! The redaction set targets the specific identifiers Aura's own code emits in its accept / dial //! paths: `peer_id` (verified client CN), `client_ip` / `local_ip` / `assigned_ip` (per-tunnel //! addresses), `source_addr` (UDP peer), `client_id` / `id` / `user` (generic id slots). //! //! ## Compatibility //! //! When `no_logs = false` (the default), [`init_filtered_tracing`] degenerates to the same //! `tracing_subscriber::fmt().with_env_filter(...).try_init()` call that lived in `main` before, //! so existing log output is unchanged for operators who did not opt in. use std::collections::HashSet; use tracing_subscriber::fmt::format::{debug_fn, Writer}; use tracing_subscriber::fmt::FormatFields; use tracing_subscriber::EnvFilter; /// Field names treated as personally-identifying and dropped from formatted output when /// `no_logs = true`. Matches the field keys Aura emits in `tracing::info!` / `warn!` macros /// across the server-accept and client-dial paths. pub const REDACTED_FIELD_NAMES: &[&str] = &[ "peer_id", "client_ip", "source_addr", "client_id", "local_ip", "user", "id", "assigned_ip", "peer", ]; /// Install the global tracing subscriber, honouring `no_logs`. /// /// * `no_logs = false`: standard `fmt` subscriber + `EnvFilter` (default `info`). /// * `no_logs = true`: same filter and formatter shell, but the per-field writer skips /// [`REDACTED_FIELD_NAMES`] so identifying fields never reach the output stream. /// /// Calls `try_init` so re-initialisation (e.g. in embedded use or repeated test setup) is a /// no-op rather than a panic. pub fn init_filtered_tracing(no_logs: bool) { let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); if no_logs { let _ = tracing_subscriber::fmt() .with_env_filter(filter) .fmt_fields(redacting_field_formatter()) .try_init(); } else { let _ = tracing_subscriber::fmt().with_env_filter(filter).try_init(); } } /// Build a [`FormatFields`] that writes every field through the default rendering EXCEPT those /// whose name matches [`REDACTED_FIELD_NAMES`] — those are silently dropped. Exposed so the /// integration tests can swap the writer for an in-memory buffer and assert the redaction. pub fn redacting_field_formatter() -> impl for<'w> FormatFields<'w> + 'static { let redacted: HashSet<&'static str> = REDACTED_FIELD_NAMES.iter().copied().collect(); debug_fn(move |w: &mut Writer<'_>, field, value| { if redacted.contains(field.name()) { // Drop the field entirely. The default formatter emits `key=value` separated by spaces, // so a no-op preserves that overall structure for the remaining fields. return Ok(()); } write!(w, "{}={:?} ", field.name(), value) }) } #[cfg(test)] mod tests { use super::*; /// The blacklist captures every identifier the spec calls out. #[test] fn redacted_set_covers_spec_identifiers() { for name in [ "peer_id", "client_ip", "source_addr", "client_id", "local_ip", "user", "id", ] { assert!( REDACTED_FIELD_NAMES.contains(&name), "missing redaction for {name}" ); } } }