Skip to main content

rullst_core/
telemetry.rs

1use tracing_core::Field;
2use tracing_core::field::Visit;
3use tracing_subscriber::Layer;
4
5/// A diagnostic layer that warns when known sensitive field names appear in tracing events.
6///
7/// This observer cannot alter an event already recorded by other layers. Callers must redact
8/// values before logging them; this layer is detection, not data-loss prevention.
9pub 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)]
49/// Initializes the OpenTelemetry OTLP pipeline for distributed tracing.
50/// This configuration connects to a local OTLP collector on port 4317.
51/// Returns a Result which can be gracefully ignored if the collector is unavailable.
52pub 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)]
94/// Initializes the OpenTelemetry OTLP pipeline for distributed tracing.
95///
96/// This configuration connects to a local OTLP collector on port 4317.
97/// Returns a Result which can be gracefully ignored if the collector is unavailable.
98pub 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}