1use tracing_core::Field;
2use tracing_core::field::Visit;
3use tracing_subscriber::Layer;
4
5pub struct RedactPersonalDataLayer;
10
11struct PrivacyVisitor {
12 has_leak: bool,
13 leaked_field: String,
14}
15
16impl Visit for PrivacyVisitor {
17 fn record_debug(&mut self, field: &Field, _value: &dyn std::fmt::Debug) {
18 let name = field.name();
19 if name == "cpf" || name == "password" || name == "email" || name == "credit_card" {
20 self.has_leak = true;
21 self.leaked_field = name.to_string();
22 }
23 }
24}
25
26#[cfg_attr(mutants, mutants::skip)]
27impl<S: tracing_core::Subscriber> Layer<S> for RedactPersonalDataLayer {
28 fn on_event(
29 &self,
30 event: &tracing_core::Event<'_>,
31 _ctx: tracing_subscriber::layer::Context<'_, S>,
32 ) {
33 let mut visitor = PrivacyVisitor {
34 has_leak: false,
35 leaked_field: String::new(),
36 };
37 event.record(&mut visitor);
38 if visitor.has_leak {
39 eprintln!(
40 "\n🚨 [RULLST TELEMETRY AUDITOR] Sensitive field name '{}' was logged. This observer cannot redact an event already emitted; remove or redact the value at the call site.\n",
41 visitor.leaked_field
42 );
43 }
44 }
45}
46
47#[cfg(feature = "telemetry")]
48#[cfg_attr(mutants, mutants::skip)]
49pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
53 use opentelemetry_otlp::WithExportConfig;
54 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
55
56 let endpoint = std::env::var("OTEL_EXPORTER_OTLP_ENDPOINT")
57 .unwrap_or_else(|_| "http://localhost:4317".to_string());
58
59 let exporter = opentelemetry_otlp::SpanExporter::builder()
60 .with_http()
61 .with_endpoint(endpoint)
62 .build()?;
63
64 let resource = opentelemetry_sdk::Resource::builder()
65 .with_service_name("rullst-app")
66 .build();
67
68 let provider = opentelemetry_sdk::trace::SdkTracerProvider::builder()
69 .with_batch_exporter(exporter)
70 .with_resource(resource)
71 .build();
72
73 opentelemetry::global::set_tracer_provider(provider.clone());
74 use opentelemetry::trace::TracerProvider as _;
75 let tracer = provider.tracer("rullst");
76
77 let telemetry_layer = tracing_opentelemetry::layer().with_tracer(tracer);
78
79 let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env()
80 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
81
82 tracing_subscriber::registry()
83 .with(filter_layer)
84 .with(tracing_subscriber::fmt::layer())
85 .with(RedactPersonalDataLayer)
86 .with(telemetry_layer)
87 .try_init()?;
88
89 Ok(())
90}
91
92#[cfg(not(feature = "telemetry"))]
93#[cfg_attr(mutants, mutants::skip)]
94pub fn init_telemetry() -> Result<(), Box<dyn std::error::Error>> {
99 use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
100
101 let filter_layer = tracing_subscriber::EnvFilter::try_from_default_env()
102 .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));
103
104 tracing_subscriber::registry()
105 .with(filter_layer)
106 .with(tracing_subscriber::fmt::layer())
107 .with(RedactPersonalDataLayer)
108 .try_init()
109 .ok();
110
111 Ok(())
112}
113
114#[cfg(test)]
115#[allow(clippy::unwrap_used)]
116mod tests {
117 use super::*;
118 use std::sync::{Arc, Mutex};
119 use tracing_core::Event;
120 use tracing_core::subscriber::Subscriber;
121 use tracing_subscriber::Layer;
122 use tracing_subscriber::layer::Context;
123
124 #[derive(Clone)]
125 struct TestLayer {
126 leaks: Arc<Mutex<Vec<String>>>,
127 }
128
129 impl<S: Subscriber> Layer<S> for TestLayer {
130 fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
131 let mut visitor = PrivacyVisitor {
132 has_leak: false,
133 leaked_field: String::new(),
134 };
135 event.record(&mut visitor);
136 if visitor.has_leak {
137 self.leaks.lock().unwrap().push(visitor.leaked_field);
138 }
139 }
140 }
141
142 #[test]
143 fn test_privacy_visitor_via_event() {
144 use tracing_subscriber::layer::SubscriberExt;
145 let leaks = Arc::new(Mutex::new(Vec::new()));
146 let layer = TestLayer {
147 leaks: leaks.clone(),
148 };
149 let subscriber = tracing_subscriber::registry().with(layer);
150 let _guard = tracing::subscriber::set_default(subscriber);
151
152 tracing::info!(cpf = "123", "test cpf");
153 tracing::info!(password = "abc", "test pwd");
154 tracing::info!(email = "a@b.com", "test email");
155 tracing::info!(credit_card = "4444", "test cc");
156 tracing::info!(safe_field = "ok", "test safe");
157
158 let l = leaks.lock().unwrap();
159 assert_eq!(l.len(), 4);
160 assert!(l.contains(&"cpf".to_string()));
161 assert!(l.contains(&"password".to_string()));
162 assert!(l.contains(&"email".to_string()));
163 assert!(l.contains(&"credit_card".to_string()));
164 }
165}